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 |