| 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 /** | |
| 20 * Ng-model directive is responsible for reading/writing to the model. | 4 * Ng-model directive is responsible for reading/writing to the model. |
| 21 * The directive itself is headless. (It does not know how to render or what | 5 * The directive itself is headless. (It does not know how to render or what |
| 22 * events to listen for.) It is meant to be used with other directives which | 6 * events to listen for.) It is meant to be used with other directives which |
| 23 * provide the rendering and listening capabilities. The directive itself | 7 * provide the rendering and listening capabilities. The directive itself |
| 24 * knows how to convert the view-value into model-value and vice versa by | 8 * knows how to convert the view-value into model-value and vice versa by |
| 25 * allowing others to register converters (To be implemented). It also | 9 * allowing others to register converters (To be implemented). It also |
| 26 * knows how to (in)validate the model and the form in which it is declared | 10 * knows how to (in)validate the model and the form in which it is declared |
| 27 * (to be implemented) | 11 * (to be implemented) |
| 28 */ | 12 */ |
| 29 @Decorator(selector: '[ng-model]') | 13 @NgDirective(selector: '[ng-model]') |
| 30 class NgModel extends NgControl implements AttachAware { | 14 class NgModel extends NgControl implements NgAttachAware { |
| 31 final Scope _scope; | 15 final NgForm _form; |
| 16 final AstParser _parser; |
| 32 | 17 |
| 18 BoundGetter getter = ([_]) => null; |
| 33 BoundSetter setter = (_, [__]) => null; | 19 BoundSetter setter = (_, [__]) => null; |
| 34 | 20 |
| 35 String _expression; | 21 var _lastValue; |
| 36 var _originalValue, _viewValue, _modelValue; | 22 String _exp; |
| 37 bool _alwaysProcessViewValue; | 23 final _validators = <NgValidatable>[]; |
| 38 bool _toBeValidated = false; | 24 |
| 25 Watch _removeWatch; |
| 26 bool _watchCollection; |
| 39 Function render = (value) => null; | 27 Function render = (value) => null; |
| 40 | 28 |
| 41 final _validators = <NgValidator>[]; | 29 NgModel(Scope _scope, dom.Element _element, Injector injector, |
| 42 NgModelConverter _converter; | 30 NgForm this._form, this._parser, NodeAttrs attrs) |
| 43 Watch _watch; | 31 : super(_scope, _element, injector) |
| 44 bool _watchCollection; | |
| 45 | |
| 46 NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs, | |
| 47 Animate animate) | |
| 48 : super(element, injector, animate) | |
| 49 { | 32 { |
| 50 _expression = attrs["ng-model"]; | 33 _exp = attrs["ng-model"]; |
| 51 watchCollection = false; | |
| 52 | |
| 53 //Since the user will never be editing the value of a select element then | |
| 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) { | |
| 62 validate(); | |
| 63 _viewValue = converter.format(value); | |
| 64 _scope.rootScope.domWrite(() => render(_viewValue)); | |
| 65 } | |
| 66 | |
| 67 void attach() { | |
| 68 watchCollection = false; | 34 watchCollection = false; |
| 69 } | 35 } |
| 70 | 36 |
| 71 /** | 37 process(value, [_]) { |
| 72 * Resets the model value to it's original (pristine) value. If the model has
been interacted | 38 validate(); |
| 73 * with by the user at all then the model will be also reset to an "untouched
" state. | 39 _scope.rootScope.domWrite(() => render(value)); |
| 74 */ | |
| 75 void reset() { | |
| 76 markAsUntouched(); | |
| 77 _processViewValue(_originalValue); | |
| 78 modelValue = _originalValue; | |
| 79 } | 40 } |
| 80 | 41 |
| 81 void onSubmit(bool valid) { | 42 attach() { |
| 82 super.onSubmit(valid); | 43 watchCollection = false; |
| 83 if (valid) _originalValue = modelValue; | 44 _scope.on('resetNgModel').listen((e) => reset()); |
| 84 } | 45 } |
| 85 | 46 |
| 86 void markAsUntouched() { | 47 reset() { |
| 87 removeInfoState(this, NgControl.NG_TOUCHED); | 48 untouched = true; |
| 88 } | 49 modelValue = _lastValue; |
| 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); | |
| 123 } | 50 } |
| 124 | 51 |
| 125 @NgAttr('name') | 52 @NgAttr('name') |
| 126 String get name => _name; | 53 get name => _name; |
| 127 void set name(value) { | 54 set name(value) { |
| 128 _name = value; | 55 _name = value; |
| 129 _parentControl.addControl(this); | 56 _parentControl.addControl(this); |
| 130 } | 57 } |
| 131 | 58 |
| 132 // TODO(misko): could we get rid of watch collection, and just always watch th
e collection? | 59 // TODO(misko): could we get rid of watch collection, and just always watch th
e collection? |
| 133 bool get watchCollection => _watchCollection; | 60 get watchCollection => _watchCollection; |
| 134 void set watchCollection(value) { | 61 set watchCollection(value) { |
| 135 if (_watchCollection == value) return; | 62 if (_watchCollection == value) return; |
| 136 | |
| 137 var onChange = (value, [_]) { | |
| 138 if (_alwaysProcessViewValue || _modelValue != value) { | |
| 139 _modelValue = value; | |
| 140 _processViewValue(value); | |
| 141 } | |
| 142 }; | |
| 143 | |
| 144 _watchCollection = value; | 63 _watchCollection = value; |
| 145 if (_watch!=null) _watch.remove(); | 64 if (_removeWatch!=null) _removeWatch.remove(); |
| 146 if (_watchCollection) { | 65 if (_watchCollection) { |
| 147 _watch = _scope.watch(_expression, (changeRecord, _) { | 66 _removeWatch = _scope.watch( |
| 148 onChange(changeRecord is CollectionChangeRecord | 67 _parser(_exp, collection: true), |
| 149 ? changeRecord.iterable | 68 (changeRecord, _) { |
| 150 : changeRecord); | 69 var value = changeRecord is CollectionChangeRecord ? changeRecord.it
erable: changeRecord; |
| 151 }, | 70 process(value); |
| 152 collection: true); | 71 }); |
| 153 } else if (_expression != null) { | 72 } else if (_exp != null) { |
| 154 _watch = _scope.watch(_expression, onChange); | 73 _removeWatch = _scope.watch(_exp, process); |
| 155 } | 74 } |
| 156 } | 75 } |
| 157 | 76 |
| 158 // TODO(misko): getters/setters need to go. We need AST here. | 77 // TODO(misko): getters/setters need to go. We need AST here. |
| 159 @NgCallback('ng-model') | 78 @NgCallback('ng-model') |
| 160 void set model(BoundExpression boundExpression) { | 79 set model(BoundExpression boundExpression) { |
| 80 getter = boundExpression; |
| 161 setter = boundExpression.assign; | 81 setter = boundExpression.assign; |
| 82 |
| 162 _scope.rootScope.runAsync(() { | 83 _scope.rootScope.runAsync(() { |
| 163 _modelValue = boundExpression(); | 84 _lastValue = modelValue; |
| 164 _originalValue = modelValue; | |
| 165 _processViewValue(_modelValue); | |
| 166 }); | 85 }); |
| 167 } | 86 } |
| 168 | 87 |
| 169 /** | 88 // TODO(misko): right now viewValue and modelValue are the same, |
| 170 * Applies the given [error] to the model. | 89 // but this needs to be changed to support converters and form validation |
| 171 */ | 90 get viewValue => modelValue; |
| 172 void addError(String error) { | 91 set viewValue(value) => modelValue = value; |
| 173 this.addErrorState(this, error); | 92 |
| 174 } | 93 get modelValue => getter(); |
| 94 set modelValue(value) => setter(value); |
| 95 |
| 96 get validators => _validators; |
| 175 | 97 |
| 176 /** | 98 /** |
| 177 * Removes the given [error] from the model. | 99 * Executes a validation on the form against each of the validation present on
the model. |
| 178 */ | 100 */ |
| 179 void removeError(String error) { | 101 validate() { |
| 180 this.removeErrorState(this, error); | 102 if (validators.isNotEmpty) { |
| 181 } | 103 validators.forEach((validator) { |
| 182 | 104 setValidity(validator.name, validator.isValid(viewValue)); |
| 183 /** | 105 }); |
| 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 { | 106 } else { |
| 216 markAsDirty(); | 107 valid = true; |
| 217 } | 108 } |
| 218 } | 109 } |
| 219 | 110 |
| 220 /** | 111 setValidity(String name, bool valid) { |
| 221 * Returns the list of validators that are registered on the model. | 112 this.updateControlValidity(this, name, valid); |
| 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. | |
| 228 */ | |
| 229 void validate() { | |
| 230 _toBeValidated = false; | |
| 231 if (validators.isNotEmpty) { | |
| 232 validators.forEach((validator) { | |
| 233 if (validator.isValid(modelValue)) { | |
| 234 removeError(validator.name); | |
| 235 } else { | |
| 236 addError(validator.name); | |
| 237 } | |
| 238 }); | |
| 239 } | |
| 240 | |
| 241 if (invalid) { | |
| 242 addInfo(NgControl.NG_INVALID); | |
| 243 } else { | |
| 244 removeInfo(NgControl.NG_INVALID); | |
| 245 } | |
| 246 } | 113 } |
| 247 | 114 |
| 248 /** | 115 /** |
| 249 * Registers a validator into the model to consider when running validate(). | 116 * Registers a validator into the model to consider when running validate(). |
| 250 */ | 117 */ |
| 251 void addValidator(NgValidator v) { | 118 addValidator(NgValidatable v) { |
| 252 validators.add(v); | 119 validators.add(v); |
| 253 validateLater(); | 120 validate(); |
| 254 } | 121 } |
| 255 | 122 |
| 256 /** | 123 /** |
| 257 * De-registers a validator from the model. | 124 * De-registers a validator from the model. |
| 258 */ | 125 */ |
| 259 void removeValidator(NgValidator v) { | 126 removeValidator(NgValidatable v) { |
| 260 validators.remove(v); | 127 validators.remove(v); |
| 261 validateLater(); | 128 validate(); |
| 262 } | 129 } |
| 263 } | 130 } |
| 264 | 131 |
| 265 /** | 132 /** |
| 266 * Usage: | 133 * Usage: |
| 267 * | 134 * |
| 268 * <input type="checkbox" | 135 * <input type="checkbox" ng-model="flag"> |
| 269 * ng-model="expr" | |
| 270 * [ng-true-value="t_expr"] | |
| 271 * [ng-false-value="f_expr"] | |
| 272 * > | |
| 273 * | 136 * |
| 274 * This creates a two way databinding between the `ng-model` expression | 137 * This creates a two way databinding between the boolean expression specified |
| 275 * and the checkbox input element state. | 138 * in ng-model and the checkbox input element in the DOM. If the ng-model value |
| 276 * | 139 * is falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is |
| 277 * If the optional `ng-true-value` is absent then: if the model expression | 140 * unchecked. Otherwise, it is checked. Likewise, when the checkbox is checked, |
| 278 * evaluates to true or to a nonzero [:num:], then the checkbox is checked; | 141 * the model value is set to true. When unchecked, it is set to false. |
| 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]. | |
| 290 */ | 142 */ |
| 291 @Decorator(selector: 'input[type=checkbox][ng-model]') | 143 @NgDirective(selector: 'input[type=checkbox][ng-model]') |
| 292 class InputCheckbox { | 144 class InputCheckboxDirective { |
| 293 final dom.CheckboxInputElement inputElement; | 145 final dom.InputElement inputElement; |
| 294 final NgModel ngModel; | 146 final NgModel ngModel; |
| 295 final NgTrueValue ngTrueValue; | 147 final NgTrueValue ngTrueValue; |
| 296 final NgFalseValue ngFalseValue; | 148 final NgFalseValue ngFalseValue; |
| 297 final Scope scope; | 149 final Scope scope; |
| 298 | 150 |
| 299 InputCheckbox(dom.Element this.inputElement, this.ngModel, | 151 InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, |
| 300 this.scope, this.ngTrueValue, this.ngFalseValue) { | 152 this.scope, this.ngTrueValue, this.ngFalseValue) { |
| 301 ngModel.render = (value) { | 153 ngModel.render = (value) { |
| 302 scope.rootScope.domWrite(() { | 154 inputElement.checked = ngTrueValue.isValue(inputElement, value); |
| 303 inputElement.checked = ngTrueValue.isValue(value); | |
| 304 }); | |
| 305 }; | 155 }; |
| 306 inputElement | 156 inputElement.onChange.listen((value) { |
| 307 ..onChange.listen((_) { | 157 ngModel.dirty = true; |
| 308 ngModel.viewValue = inputElement.checked | 158 ngModel.viewValue = inputElement.checked |
| 309 ? ngTrueValue.value : ngFalseValue.value; | 159 ? ngTrueValue.readValue(inputElement) |
| 310 }) | 160 : ngFalseValue.readValue(inputElement); |
| 311 ..onBlur.listen((e) { | 161 }); |
| 312 ngModel.markAsTouched(); | |
| 313 }); | |
| 314 } | 162 } |
| 315 } | 163 } |
| 316 | 164 |
| 317 /** | 165 /** |
| 318 * Usage: | 166 * Usage: |
| 319 * | 167 * |
| 320 * <input type="text|url|password|email" ng-model="myModel"> | 168 * <input type="text|url|password|email" ng-model="myModel"> |
| 321 * <textarea ng-model="myModel"></textarea> | 169 * <textarea ng-model="myModel"></textarea> |
| 322 * | 170 * |
| 323 * This creates a two-way binding between any string-based input element | 171 * This creates a two-way binding between any string-based input element |
| 324 * (both `<input>` and `<textarea>`) so long as the ng-model attribute is | 172 * (both <input> and <textarea>) so long as the ng-model attribute is |
| 325 * present on the input element. Whenever the value of the input element | 173 * present on the input element. Whenever the value of the input element |
| 326 * changes then the matching model property on the scope will be updated | 174 * changes then the matching model property on the scope will be updated |
| 327 * as well as the other way around (when the scope property is updated). | 175 * as well as the other way around (when the scope property is updated). |
| 328 * | 176 * |
| 329 */ | 177 */ |
| 330 @Decorator(selector: 'textarea[ng-model]') | 178 @NgDirective(selector: 'textarea[ng-model]') |
| 331 @Decorator(selector: 'input[type=text][ng-model]') | 179 @NgDirective(selector: 'input[type=text][ng-model]') |
| 332 @Decorator(selector: 'input[type=password][ng-model]') | 180 @NgDirective(selector: 'input[type=password][ng-model]') |
| 333 @Decorator(selector: 'input[type=url][ng-model]') | 181 @NgDirective(selector: 'input[type=url][ng-model]') |
| 334 @Decorator(selector: 'input[type=email][ng-model]') | 182 @NgDirective(selector: 'input[type=email][ng-model]') |
| 335 @Decorator(selector: 'input[type=search][ng-model]') | 183 @NgDirective(selector: 'input[type=search][ng-model]') |
| 336 class InputTextLike { | 184 class InputTextLikeDirective { |
| 337 final dom.Element inputElement; | 185 final dom.Element inputElement; |
| 338 final NgModel ngModel; | 186 final NgModel ngModel; |
| 339 final Scope scope; | 187 final Scope scope; |
| 340 String _inputType; | 188 String _inputType; |
| 341 | 189 |
| 342 get typedValue => (inputElement as dynamic).value; | 190 get typedValue => (inputElement as dynamic).value; |
| 343 void set typedValue(value) { | 191 set typedValue(value) => (inputElement as dynamic).value = (value == null) ? |
| 344 (inputElement as dynamic).value = (value == null) ? '' : value.toString(); | 192 '' : |
| 345 } | 193 value.toString(); |
| 346 | 194 |
| 347 InputTextLike(this.inputElement, this.ngModel, this.scope) { | 195 InputTextLikeDirective(this.inputElement, this.ngModel, this.scope) { |
| 348 ngModel.render = (value) { | 196 ngModel.render = (value) { |
| 349 scope.rootScope.domWrite(() { | 197 if (value == null) value = ''; |
| 350 if (value == null) value = ''; | |
| 351 | 198 |
| 352 var currentValue = typedValue; | 199 var currentValue = typedValue; |
| 353 if (value != currentValue && !(value is num && currentValue is num && | 200 if (value != currentValue && !(value is num && currentValue is num && |
| 354 value.isNaN && currentValue.isNaN)) { | 201 value.isNaN && currentValue.isNaN)) { |
| 355 typedValue = value; | 202 typedValue = value; |
| 356 } | 203 } |
| 357 }); | |
| 358 }; | 204 }; |
| 359 inputElement | 205 inputElement |
| 360 ..onChange.listen(processValue) | 206 ..onChange.listen(processValue) |
| 361 ..onInput.listen(processValue) | 207 ..onInput.listen(processValue) |
| 362 ..onBlur.listen((e) { | 208 ..onBlur.listen((e) { |
| 363 ngModel.markAsTouched(); | 209 if (ngModel.touched == null || ngModel.touched == false) { |
| 210 ngModel.touched = true; |
| 211 } |
| 364 }); | 212 }); |
| 365 } | 213 } |
| 366 | 214 |
| 367 void processValue([_]) { | 215 processValue([_]) { |
| 368 var value = typedValue; | 216 var value = typedValue; |
| 369 if (value != ngModel.viewValue) ngModel.viewValue = value; | 217 if (value != ngModel.viewValue) { |
| 218 ngModel.dirty = true; |
| 219 ngModel.viewValue = value; |
| 220 } |
| 370 ngModel.validate(); | 221 ngModel.validate(); |
| 371 } | 222 } |
| 372 } | 223 } |
| 373 | 224 |
| 374 /** | 225 /** |
| 375 * Usage: | 226 * Usage: |
| 376 * | 227 * |
| 377 * <input type="number|range" ng-model="myModel"> | 228 * <input type="number|range" ng-model="myModel"> |
| 378 * | 229 * |
| 379 * Model: | 230 * Model: |
| 380 * | 231 * |
| 381 * num myModel; | 232 * num myModel; |
| 382 * | 233 * |
| 383 * This creates a two-way binding between the input and the named model property | 234 * This creates a two-way binding between the input and the named model property |
| 384 * (e.g., myModel in the example above). When processing the input, its value is | 235 * (e.g., myModel in the example above). When processing the input, its value is |
| 385 * read as a [:num:], via the [dom.InputElement.valueAsNumber] field. If the | 236 * read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input |
| 386 * input text does not represent a number, then the model is appropriately set | 237 * text does not represent a number, then the model is appropriately set to |
| 387 * to [double.NAN]. Setting the model property to [null] will clear the input. | 238 * [double.NAN]. Setting the model property to [null] will clear the input. |
| 388 * Setting the model to [double.NAN] will have no effect (input will be left | 239 * Setting the model to [double.NAN] will have no effect (input will be left |
| 389 * unchanged). | 240 * unchanged). |
| 390 */ | 241 */ |
| 391 @Decorator(selector: 'input[type=number][ng-model]') | 242 @NgDirective(selector: 'input[type=number][ng-model]') |
| 392 @Decorator(selector: 'input[type=range][ng-model]') | 243 @NgDirective(selector: 'input[type=range][ng-model]') |
| 393 class InputNumberLike { | 244 class InputNumberLikeDirective { |
| 394 final dom.InputElement inputElement; | 245 final dom.InputElement inputElement; |
| 395 final NgModel ngModel; | 246 final NgModel ngModel; |
| 396 final Scope scope; | 247 final Scope scope; |
| 397 | 248 |
| 398 | 249 num get typedValue => inputElement.valueAsNumber; |
| 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 | |
| 402 void set typedValue(num value) { | 250 void set typedValue(num value) { |
| 403 // [chalin, 2014-02-16] This post | 251 // [chalin, 2014-02-16] This post |
| 404 // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.h
tml | 252 // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.h
tml |
| 405 // suggests that setting `valueAsNumber` to null should clear the field, but | 253 // suggests that setting `valueAsNumber` to null should clear the field, but |
| 406 // it does not. [TODO: put BUG/ISSUE number here]. We implement a | 254 // it does not. [TODO: put BUG/ISSUE number here]. We implement a |
| 407 // workaround by setting `value`. Clean-up once the bug is fixed. | 255 // workaround by setting `value`. Clean-up once the bug is fixed. |
| 408 if (value == null) { | 256 if (value == null) { |
| 409 inputElement.value = null; | 257 inputElement.value = null; |
| 410 } else { | 258 } else { |
| 411 // We can't use inputElement.valueAsNumber due to http://dartbug.com/15788 | 259 inputElement.valueAsNumber = value; |
| 412 inputElement.value = "$value"; | |
| 413 } | 260 } |
| 414 } | 261 } |
| 415 | 262 |
| 416 InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope) { | 263 InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.sco
pe) { |
| 417 ngModel.render = (value) { | 264 ngModel.render = (value) { |
| 418 scope.rootScope.domWrite(() { | 265 if (value != typedValue |
| 419 if (value != typedValue | 266 && (value == null || value is num && !value.isNaN)) { |
| 420 && (value == null || value is num && !value.isNaN)) { | 267 typedValue = value; |
| 421 typedValue = value; | 268 } |
| 422 } | |
| 423 }); | |
| 424 }; | 269 }; |
| 425 inputElement | 270 inputElement |
| 426 ..onChange.listen(relaxFnArgs(processValue)) | 271 ..onChange.listen(relaxFnArgs(processValue)) |
| 427 ..onInput.listen(relaxFnArgs(processValue)) | 272 ..onInput.listen(relaxFnArgs(processValue)); |
| 428 ..onBlur.listen((e) { | |
| 429 ngModel.markAsTouched(); | |
| 430 }); | |
| 431 } | 273 } |
| 432 | 274 |
| 433 void processValue() { | 275 processValue() { |
| 434 num value = typedValue; | 276 num value = typedValue; |
| 435 if (value != ngModel.viewValue) { | 277 if (value != ngModel.viewValue) { |
| 278 ngModel.dirty = true; |
| 436 scope.eval(() => ngModel.viewValue = value); | 279 scope.eval(() => ngModel.viewValue = value); |
| 437 } | 280 } |
| 438 ngModel.validate(); | 281 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(); | |
| 622 } | 282 } |
| 623 } | 283 } |
| 624 | 284 |
| 625 class _UidCounter { | 285 class _UidCounter { |
| 626 static final int CHAR_0 = "0".codeUnitAt(0); | 286 static final int CHAR_0 = "0".codeUnitAt(0); |
| 627 static final int CHAR_9 = "9".codeUnitAt(0); | 287 static final int CHAR_9 = "9".codeUnitAt(0); |
| 628 static final int CHAR_A = "A".codeUnitAt(0); | 288 static final int CHAR_A = "A".codeUnitAt(0); |
| 629 static final int CHAR_Z = "Z".codeUnitAt(0); | 289 static final int CHAR_Z = "Z".codeUnitAt(0); |
| 630 final charCodes = [CHAR_0, CHAR_0, CHAR_0]; | 290 List charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
| 631 | 291 |
| 632 String next() { | 292 String next() { |
| 633 for (int i = charCodes.length - 1; i >= 0; i--) { | 293 for (int i = charCodes.length - 1; i >= 0; i--) { |
| 634 int code = charCodes[i]; | 294 int code = charCodes[i]; |
| 635 if (code == CHAR_9) { | 295 if (code == CHAR_9) { |
| 636 charCodes[i] = CHAR_A; | 296 charCodes[i] = CHAR_A; |
| 637 return new String.fromCharCodes(charCodes); | 297 return new String.fromCharCodes(charCodes); |
| 638 } else if (code == CHAR_Z) { | 298 } else if (code == CHAR_Z) { |
| 639 charCodes[i] = CHAR_0; | 299 charCodes[i] = CHAR_0; |
| 640 } else { | 300 } else { |
| 641 charCodes[i] = code + 1; | 301 charCodes[i] = code + 1; |
| 642 return new String.fromCharCodes(charCodes); | 302 return new String.fromCharCodes(charCodes); |
| 643 } | 303 } |
| 644 } | 304 } |
| 645 charCodes.insert(0, CHAR_0); | 305 charCodes.insert(0, CHAR_0); |
| 646 return new String.fromCharCodes(charCodes); | 306 return new String.fromCharCodes(charCodes); |
| 647 } | 307 } |
| 648 } | 308 } |
| 649 | 309 |
| 650 final _uidCounter = new _UidCounter(); | 310 final _uidCounter = new _UidCounter(); |
| 651 | 311 |
| 652 /** | 312 /** |
| 653 * Usage: | 313 * Use `ng-value` directive with `<input type="radio">` or `<option>` to |
| 654 * | 314 * allow binding to values other then strings. This is needed since the |
| 655 * <input type=radio ng-model=model [ng-value=expr]> | 315 * `value` attribute on DOM element `<input type="radio" value="foo">` can |
| 656 * | 316 * only be a string. With `ng-value` one can bind to any object. |
| 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]. | |
| 669 */ | 317 */ |
| 670 @Decorator(selector: 'input[type=radio][ng-model][ng-value]') | 318 @NgDirective(selector: '[ng-value]') |
| 671 @Decorator(selector: 'option[ng-value]') | |
| 672 class NgValue { | 319 class NgValue { |
| 673 static Module _module = new Module()..type(NgValue); | |
| 674 static Module moduleFactory() => _module; | |
| 675 | |
| 676 final dom.Element element; | 320 final dom.Element element; |
| 677 var _value; | 321 @NgOneWay('ng-value') |
| 322 var value; |
| 678 | 323 |
| 679 NgValue(this.element); | 324 NgValue(this.element); |
| 680 | 325 |
| 681 @NgOneWay('ng-value') | 326 readValue(dom.Element element) { |
| 682 void set value(val) { this._value = val; } | 327 assert(this.element == null || element == this.element); |
| 683 dynamic get value => _value == null ? (element as dynamic).value : _value; | 328 return this.element == null ? (element as dynamic).value : value; |
| 329 } |
| 684 } | 330 } |
| 685 | 331 |
| 686 /** | 332 /** |
| 687 * Usage: | 333 * `ng-true-value` allows you to select any expression to be set to |
| 688 * | 334 * `ng-model` when checkbox is selected on `<input type="checkbox">`. |
| 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]. | |
| 696 */ | 335 */ |
| 697 @Decorator(selector: 'input[type=checkbox][ng-model][ng-true-value]') | 336 @NgDirective(selector: '[ng-true-value]') |
| 698 class NgTrueValue { | 337 class NgTrueValue { |
| 699 final dom.Element element; | 338 final dom.Element element; |
| 700 @NgOneWay('ng-true-value') | 339 @NgOneWay('ng-true-value') |
| 701 var value = true; | 340 var value; |
| 702 | 341 |
| 703 NgTrueValue([this.element]); | 342 NgTrueValue(this.element); |
| 704 | 343 |
| 705 bool isValue(val) => element == null ? toBool(val) : val == value; | 344 readValue(dom.Element element) { |
| 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 } |
| 706 } | 353 } |
| 707 | 354 |
| 708 /** | 355 /** |
| 709 * Usage: | 356 * `ng-false-value` allows you to select any expression to be set to |
| 710 * | 357 * `ng-model` when checkbox is deselected<input type="checkbox">`. |
| 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]. | |
| 718 */ | 358 */ |
| 719 @Decorator(selector: 'input[type=checkbox][ng-model][ng-false-value]') | 359 @NgDirective(selector: '[ng-false-value]') |
| 720 class NgFalseValue { | 360 class NgFalseValue { |
| 721 final dom.Element element; | 361 final dom.Element element; |
| 722 @NgOneWay('ng-false-value') | 362 @NgOneWay('ng-false-value') |
| 723 var value = false; | 363 var value; |
| 724 | 364 |
| 725 NgFalseValue([this.element]); | 365 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 } |
| 726 } | 371 } |
| 727 | 372 |
| 728 /** | 373 /** |
| 729 * Usage: | 374 * Usage: |
| 730 * | 375 * |
| 731 * <input type="radio" ng-model="category"> | 376 * <input type="radio" ng-model="category"> |
| 732 * | 377 * |
| 733 * This creates a two way databinding between the expression specified in | 378 * This creates a two way databinding between the expression specified in |
| 734 * ng-model and the range input elements in the DOM. If the ng-model value is | 379 * ng-model and the range input elements in the DOM. If the ng-model value is |
| 735 * set to a value not corresponding to one of the radio elements, then none of | 380 * set to a value not corresponding to one of the radio elements, then none of |
| 736 * the radio elements will be check. Otherwise, only the corresponding input | 381 * the radio elements will be check. Otherwise, only the corresponding input |
| 737 * element in the group is checked. Likewise, when a radio button element is | 382 * element in the group is checked. Likewise, when a radio button element is |
| 738 * checked, the model is updated with its value. Radio buttons that have a | 383 * checked, the model is updated with its value. Radio buttons that have a |
| 739 * `name` attribute are left alone. Those that are missing the attribute will | 384 * `name` attribute are left alone. Those that are missing the attribute will |
| 740 * have a unique `name` assigned to them. This sequence goes `001`, `001`, ... | 385 * have a unique `name` assigned to them. This sequence goes `001`, `001`, ... |
| 741 * `009`, `00A`, `00Z`, `010`, and so on using more than 3 characters for the | 386 * `009`, `00A`, `00Z`, `010`, … and so on using more than 3 characters for the |
| 742 * name when the counter overflows. | 387 * name when the counter overflows. |
| 743 */ | 388 */ |
| 744 @Decorator( | 389 @NgDirective(selector: 'input[type=radio][ng-model]') |
| 745 selector: 'input[type=radio][ng-model]', | 390 class InputRadioDirective { |
| 746 module: NgValue.moduleFactory) | |
| 747 class InputRadio { | |
| 748 final dom.RadioButtonInputElement radioButtonElement; | 391 final dom.RadioButtonInputElement radioButtonElement; |
| 749 final NgModel ngModel; | 392 final NgModel ngModel; |
| 750 final NgValue ngValue; | 393 final NgValue ngValue; |
| 751 final Scope scope; | 394 final Scope scope; |
| 752 | 395 |
| 753 InputRadio(dom.Element this.radioButtonElement, this.ngModel, | 396 InputRadioDirective(dom.Element this.radioButtonElement, this.ngModel, |
| 754 this.scope, this.ngValue, NodeAttrs attrs) { | 397 this.scope, this.ngValue, NodeAttrs attrs) { |
| 755 // If there's no "name" set, we'll set a unique name. This ensures | 398 // If there's no "name" set, we'll set a unique name. This ensures |
| 756 // less surprising behavior about which radio buttons are grouped together. | 399 // less surprising behavior about which radio buttons are grouped together. |
| 757 if (attrs['name'] == '' || attrs['name'] == null) { | 400 if (attrs['name'] == '' || attrs['name'] == null) { |
| 758 attrs["name"] = _uidCounter.next(); | 401 attrs["name"] = _uidCounter.next(); |
| 759 } | 402 } |
| 760 ngModel.render = (value) { | 403 ngModel.render = (value) { |
| 761 scope.rootScope.domWrite(() { | 404 radioButtonElement.checked = (value == ngValue.readValue(radioButtonElemen
t)); |
| 762 radioButtonElement.checked = (value == ngValue.value); | |
| 763 }); | |
| 764 }; | 405 }; |
| 765 radioButtonElement | 406 radioButtonElement.onClick.listen((_) { |
| 766 ..onClick.listen((_) { | 407 if (radioButtonElement.checked) { |
| 767 if (radioButtonElement.checked) ngModel.viewValue = ngValue.value; | 408 ngModel.dirty = true; |
| 768 }) | 409 ngModel.viewValue = ngValue.readValue(radioButtonElement); |
| 769 ..onBlur.listen((e) { | 410 } |
| 770 ngModel.markAsTouched(); | 411 }); |
| 771 }); | |
| 772 } | 412 } |
| 773 } | 413 } |
| 774 | 414 |
| 775 /** | 415 /** |
| 776 * Usage (span could be replaced with any element which supports text content, s
uch as `p`): | 416 * Usage (span could be replaced with any element which supports text content, s
uch as `p`): |
| 777 * | 417 * |
| 778 * <span contenteditable= ng-model="name"> | 418 * <span contenteditable= ng-model="name"> |
| 779 * | 419 * |
| 780 * This creates a two way databinding between the expression specified in | 420 * This creates a two way databinding between the expression specified in |
| 781 * ng-model and the html element in the DOM. If the ng-model value is | 421 * ng-model and the html element in the DOM. If the ng-model value is |
| 782 * `null`, it is treated as equivalent to the empty string for rendering | 422 * `null`, it is treated as equivalent to the empty string for rendering |
| 783 * purposes. | 423 * purposes. |
| 784 */ | 424 */ |
| 785 @Decorator(selector: '[contenteditable][ng-model]') | 425 @NgDirective(selector: '[contenteditable][ng-model]') |
| 786 class ContentEditable extends InputTextLike { | 426 class ContentEditableDirective extends InputTextLikeDirective { |
| 787 ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope) | 427 ContentEditableDirective(dom.Element inputElement, NgModel ngModel, |
| 428 Scope scope) |
| 788 : super(inputElement, ngModel, scope); | 429 : super(inputElement, ngModel, scope); |
| 789 | 430 |
| 790 // The implementation is identical to InputTextLike but use innerHtml instead
of value | 431 // The implementation is identical to InputTextLikeDirective but use innerHtml
instead of value |
| 791 String get typedValue => (inputElement as dynamic).innerHtml; | 432 get typedValue => (inputElement as dynamic).innerHtml; |
| 792 void set typedValue(String value) { | 433 set typedValue(String value) => |
| 793 (inputElement as dynamic).innerHtml = (value == null) ? '' : value; | 434 (inputElement as dynamic).innerHtml = (value == null) ? '' : value; |
| 794 } | |
| 795 } | 435 } |
| OLD | NEW |