| OLD | NEW |
| 1 part of angular.directive; | 1 part of angular.directive; |
| 2 | 2 |
| 3 /** | 3 /** |
| 4 * Ng-model directive is responsible for reading/writing to the model. | 4 * 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 | 5 * 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 | 6 * events to listen for.) It is meant to be used with other directives which |
| 7 * provide the rendering and listening capabilities. The directive itself | 7 * provide the rendering and listening capabilities. The directive itself |
| 8 * 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 |
| 9 * allowing others to register converters (To be implemented). It also | 9 * allowing others to register converters (To be implemented). It also |
| 10 * knwos 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 |
| 11 * (to be implemented) | 11 * (to be implemented) |
| 12 */ | 12 */ |
| 13 @NgDirective( | 13 @NgDirective( |
| 14 selector: '[ng-model]', | 14 selector: '[ng-model]') |
| 15 map: const {'ng-model': '&model'}) | 15 class NgModel extends NgControl { |
| 16 class NgModel { | 16 final NgForm _form; |
| 17 final dom.Element _element; |
| 17 final Scope _scope; | 18 final Scope _scope; |
| 18 | 19 |
| 19 Getter getter = ([_]) => null; | 20 Getter getter = ([_]) => null; |
| 20 Setter setter = (_, [__]) => null; | 21 Setter setter = (_, [__]) => null; |
| 22 |
| 21 String _exp; | 23 String _exp; |
| 24 String _name; |
| 22 | 25 |
| 26 final List<_NgModelValidator> _validators = new List<_NgModelValidator>(); |
| 27 final Map<String, bool> currentErrors = new Map<String, bool>(); |
| 23 | 28 |
| 24 Function _removeWatch = () => null; | 29 Function _removeWatch = () => null; |
| 25 bool _watchCollection; | 30 bool _watchCollection; |
| 26 | 31 |
| 27 Function render = (value) => null; | 32 Function render = (value) => null; |
| 28 | 33 |
| 29 NgModel(this._scope, NodeAttrs attrs) { | 34 NgModel(this._scope, NodeAttrs attrs, [dom.Element this._element, NgForm this.
_form]) { |
| 30 _exp = 'ng-model=${attrs["ng-model"]}'; | 35 _exp = 'ng-model=${attrs["ng-model"]}'; |
| 31 watchCollection = false; | 36 watchCollection = false; |
| 37 |
| 38 _form.addControl(this); |
| 39 pristine = true; |
| 40 } |
| 41 |
| 42 get element => _element; |
| 43 |
| 44 @NgAttr('name') |
| 45 get name => _name; |
| 46 set name(value) { |
| 47 _name = value; |
| 48 _form.addControl(this); |
| 32 } | 49 } |
| 33 | 50 |
| 34 get watchCollection => _watchCollection; | 51 get watchCollection => _watchCollection; |
| 35 set watchCollection(value) { | 52 set watchCollection(value) { |
| 36 if (_watchCollection == value) return; | 53 if (_watchCollection == value) return; |
| 37 _watchCollection = value; | 54 _watchCollection = value; |
| 38 _removeWatch(); | 55 _removeWatch(); |
| 39 if (_watchCollection) { | 56 if (_watchCollection) { |
| 40 _removeWatch = _scope.$watchCollection((s) => getter(), (value) => render(
value), _exp); | 57 _removeWatch = _scope.$watchCollection((s) => getter(), (value) => render(
value), _exp); |
| 41 } else { | 58 } else { |
| 42 _removeWatch = _scope.$watch((s) => getter(), (value) => render(value), _e
xp); | 59 _removeWatch = _scope.$watch((s) => getter(), (value) => render(value), _e
xp); |
| 43 } | 60 } |
| 44 } | 61 } |
| 45 | 62 |
| 63 @NgCallback('ng-model') |
| 46 set model(BoundExpression boundExpression) { | 64 set model(BoundExpression boundExpression) { |
| 47 getter = boundExpression; | 65 getter = boundExpression; |
| 48 setter = boundExpression.assign; | 66 setter = boundExpression.assign; |
| 49 } | 67 } |
| 50 | 68 |
| 51 // TODO(misko): right now viewValue and modelValue are the same, | 69 // TODO(misko): right now viewValue and modelValue are the same, |
| 52 // but this needs to be changed to support converters and form validation | 70 // but this needs to be changed to support converters and form validation |
| 53 get viewValue => modelValue; | 71 get viewValue => modelValue; |
| 54 set viewValue(value) => modelValue = value; | 72 set viewValue(value) => modelValue = value; |
| 55 | 73 |
| 56 get modelValue => getter(); | 74 get modelValue => getter(); |
| 57 set modelValue(value) => setter(value); | 75 set modelValue(value) => setter(value); |
| 76 |
| 77 get validators => _validators; |
| 78 |
| 79 /** |
| 80 * Executes a validation on the form against each of the validation present on
the model. |
| 81 */ |
| 82 validate() { |
| 83 if(validators.length > 0) { |
| 84 validators.forEach((validator) { |
| 85 setValidity(validator.name, validator.isValid()); |
| 86 }); |
| 87 } else { |
| 88 valid = true; |
| 89 } |
| 90 } |
| 91 |
| 92 /** |
| 93 * Sets the validity status of the given errorType on the model. Depending on
if |
| 94 * valid or invalid, the matching CSS classes will be added/removed on the inp
ut |
| 95 * element associated with the model. If any errors exist on the model then in
valid |
| 96 * will be set to true otherwise valid will be set to true. |
| 97 * |
| 98 * * [errorType] - The name of the error (e.g. required, url, number, etc...). |
| 99 * * [isValid] - Whether or not the given error is valid or not (false would m
ean the error is real). |
| 100 */ |
| 101 setValidity(String errorType, bool isValid) { |
| 102 if(isValid) { |
| 103 if(currentErrors.containsKey(errorType)) { |
| 104 currentErrors.remove(errorType); |
| 105 } |
| 106 if(valid != true && currentErrors.isEmpty) { |
| 107 valid = true; |
| 108 } |
| 109 } else if(!currentErrors.containsKey(errorType)) { |
| 110 currentErrors[errorType] = true; |
| 111 invalid = true; |
| 112 } |
| 113 |
| 114 if(_form != null) { |
| 115 _form.setValidity(this, errorType, isValid); |
| 116 } |
| 117 } |
| 118 |
| 119 /** |
| 120 * Registers a validator into the model to consider when running validate(). |
| 121 */ |
| 122 addValidator(_NgModelValidator v) { |
| 123 validators.add(v); |
| 124 validate(); |
| 125 } |
| 126 |
| 127 /** |
| 128 * De-registers a validator from the model. |
| 129 */ |
| 130 removeValidator(_NgModelValidator v) { |
| 131 validators.remove(v); |
| 132 validate(); |
| 133 } |
| 134 |
| 135 /** |
| 136 * Removes the model from the control/form. |
| 137 */ |
| 138 destroy() { |
| 139 _form.removeControl(this); |
| 140 } |
| 58 } | 141 } |
| 59 | 142 |
| 60 /** | 143 /** |
| 61 * Usage: | 144 * Usage: |
| 62 * | 145 * |
| 63 * <input type="checkbox" ng-model="flag"> | 146 * <input type="checkbox" ng-model="flag"> |
| 64 * | 147 * |
| 65 * This creates a two way databinding between the boolean expression specified i
n | 148 * This creates a two way databinding between the boolean expression specified i
n |
| 66 * ng-model and the checkbox input element in the DOM. If the ng-model value is | 149 * ng-model and the checkbox input element in the DOM. If the ng-model value is |
| 67 * falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is unchecked. | 150 * falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is unchecked. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 79 InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, this.scope
) { | 162 InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, this.scope
) { |
| 80 ngModel.render = (value) { | 163 ngModel.render = (value) { |
| 81 inputElement.checked = value == null ? false : toBool(value); | 164 inputElement.checked = value == null ? false : toBool(value); |
| 82 }; | 165 }; |
| 83 inputElement.onChange.listen((value) { | 166 inputElement.onChange.listen((value) { |
| 84 scope.$apply(() => ngModel.viewValue = inputElement.checked); | 167 scope.$apply(() => ngModel.viewValue = inputElement.checked); |
| 85 }); | 168 }); |
| 86 } | 169 } |
| 87 } | 170 } |
| 88 | 171 |
| 89 | 172 /** |
| 90 abstract class _InputTextlikeDirective { | 173 * Usage: |
| 174 * |
| 175 * <input type="text|number|url|password|email" ng-model="myModel"> |
| 176 * <textarea ng-model="myModel"></textarea> |
| 177 * |
| 178 * This creates a two-way binding between any string-based input element |
| 179 * (both <input> and <textarea>) so long as the ng-model attribute is |
| 180 * present on the input element. Whenever the value of the input element |
| 181 * changes then the matching model property on the scope will be updated |
| 182 * as well as the other way around (when the scope property is updated). |
| 183 * |
| 184 */ |
| 185 @NgDirective(selector: 'textarea[ng-model]') |
| 186 @NgDirective(selector: 'input[type=text][ng-model]') |
| 187 @NgDirective(selector: 'input[type=password][ng-model]') |
| 188 @NgDirective(selector: 'input[type=url][ng-model]') |
| 189 @NgDirective(selector: 'input[type=email][ng-model]') |
| 190 @NgDirective(selector: 'input[type=number][ng-model]') |
| 191 class InputTextLikeDirective { |
| 91 dom.Element inputElement; | 192 dom.Element inputElement; |
| 92 NgModel ngModel; | 193 NgModel ngModel; |
| 93 Scope scope; | 194 Scope scope; |
| 195 String _inputType; |
| 94 | 196 |
| 95 get typedValue => (inputElement as dynamic).value; | 197 get typedValue => (inputElement as dynamic).value; |
| 96 set typedValue(String value) => (inputElement as dynamic).value = (value == nu
ll) ? '' : value; | 198 set typedValue(value) => (inputElement as dynamic).value = (value == null) ? '
' : value.toString(); |
| 97 | 199 |
| 98 _InputTextlikeDirective(dom.Element this.inputElement, this.ngModel, this.scop
e) { | 200 InputTextLikeDirective(dom.Element this.inputElement, NgModel this.ngModel, Sc
ope this.scope) { |
| 99 ngModel.render = (value) { | 201 ngModel.render = (value) { |
| 100 if (value == null) value = ''; | 202 if (value == null) value = ''; |
| 101 | 203 |
| 102 var currentValue = typedValue; | 204 var currentValue = typedValue; |
| 103 if (value != currentValue && !(value is num && currentValue is num && valu
e.isNaN && currentValue.isNaN)) { | 205 if (value != currentValue && !(value is num && currentValue is num && valu
e.isNaN && currentValue.isNaN)) { |
| 104 typedValue = value; | 206 typedValue = value; |
| 105 } | 207 } |
| 106 }; | 208 }; |
| 107 inputElement.onChange.listen(relaxFnArgs(processValue)); | 209 inputElement.onChange.listen(relaxFnArgs(processValue)); |
| 108 inputElement.onKeyDown.listen((e) { | 210 inputElement.onKeyDown.listen((e) { |
| 109 new async.Timer(Duration.ZERO, processValue); | 211 new async.Timer(Duration.ZERO, processValue); |
| 110 scope.$skipAutoDigest(); | 212 scope.$skipAutoDigest(); |
| 111 }); | 213 }); |
| 112 } | 214 } |
| 113 | 215 |
| 114 processValue() { | 216 processValue() { |
| 217 ngModel.validate(); |
| 115 var value = typedValue; | 218 var value = typedValue; |
| 116 if (value != ngModel.viewValue) { | 219 if (value != ngModel.viewValue) { |
| 117 scope.$apply(() => ngModel.viewValue = value); | 220 scope.$apply(() => ngModel.viewValue = value); |
| 118 } | 221 } |
| 119 } | 222 } |
| 120 } | 223 } |
| 121 | 224 |
| 122 /** | |
| 123 * Usage: | |
| 124 * | |
| 125 * <input type="text" ng-model="name"> | |
| 126 * | |
| 127 * This creates a two way databinding between the expression specified in | |
| 128 * ng-model and the text input element in the DOM. If the ng-model value is | |
| 129 * `null`, it is treated as equivalent to the empty string for rendering | |
| 130 * purposes. | |
| 131 */ | |
| 132 @NgDirective(selector: 'input[type=text][ng-model]') | |
| 133 class InputTextDirective extends _InputTextlikeDirective { | |
| 134 InputTextDirective(dom.Element inputElement, NgModel ngModel, Scope scope): | |
| 135 super(inputElement, ngModel, scope); | |
| 136 | |
| 137 } | |
| 138 | |
| 139 /** | |
| 140 * Usage: | |
| 141 * | |
| 142 * <input type="password" ng-model="name"> | |
| 143 * | |
| 144 * This creates a two way databinding between the expression specified in | |
| 145 * ng-model and the password input element in the DOM. If the ng-model value is | |
| 146 * `null`, it is treated as equivalent to the empty string for rendering | |
| 147 * purposes. | |
| 148 */ | |
| 149 @NgDirective(selector: 'input[type=password][ng-model]') | |
| 150 class InputPasswordDirective extends _InputTextlikeDirective { | |
| 151 InputPasswordDirective(dom.Element inputElement, NgModel ngModel, Scope scope)
: | |
| 152 super(inputElement, ngModel, scope); | |
| 153 } | |
| 154 | |
| 155 /** | |
| 156 * Usage: | |
| 157 * | |
| 158 * <textarea ng-model="text"> | |
| 159 * | |
| 160 * This creates a two way databinding between the expression specified in | |
| 161 * ng-model and the textarea element in the DOM. If the ng-model value is | |
| 162 * `null`, it is treated as equivalent to the empty string for rendering | |
| 163 * purposes. | |
| 164 */ | |
| 165 @NgDirective(selector: 'textarea[ng-model]') | |
| 166 class TextAreaDirective extends _InputTextlikeDirective { | |
| 167 TextAreaDirective(dom.Element inputElement, NgModel ngModel, Scope scope): | |
| 168 super(inputElement, ngModel, scope); | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * Usage: | |
| 173 * | |
| 174 * <input type="number" ng-model="name"> | |
| 175 * | |
| 176 * This creates a two way databinding between the expression specified in | |
| 177 * ng-model and the number input element in the DOM. If the ng-model value is | |
| 178 * `null` or `NaN`, the DOM element is not updated. If the value in the DOM | |
| 179 * element is an invalid number, then the expression specified by the `ng-model` | |
| 180 * is set to null., | |
| 181 */ | |
| 182 @NgDirective(selector: 'input[type=number][ng-model]') | |
| 183 class InputNumberDirective extends _InputTextlikeDirective { | |
| 184 InputNumberDirective(dom.Element inputElement, NgModel ngModel, Scope scope): | |
| 185 super(inputElement, ngModel, scope); | |
| 186 | |
| 187 get typedValue => (inputElement as dom.InputElement).valueAsNumber; | |
| 188 | |
| 189 set typedValue(var value) { | |
| 190 if (value != null && value is num) { | |
| 191 num number = value as num; | |
| 192 if (!value.isNaN) { | |
| 193 (inputElement as dom.InputElement).valueAsNumber = value; | |
| 194 } | |
| 195 } | |
| 196 } | |
| 197 } | |
| 198 | |
| 199 /** | |
| 200 * Usage: | |
| 201 * | |
| 202 * <input type="email" ng-model="emailAddress"> | |
| 203 * | |
| 204 * This creates a two way databinding between the expression specified in | |
| 205 * ng-model and the email input element in the DOM. If the ng-model value is | |
| 206 * `null`, the DOM element is not updated. If the value in the DOM element is | |
| 207 * an invalid e-mail address, then the expression specified by the `ng-model` is | |
| 208 * set to null., | |
| 209 */ | |
| 210 @NgDirective(selector: 'input[type=email][ng-model]') | |
| 211 class InputEmailDirective extends _InputTextlikeDirective { | |
| 212 static final EMAIL_REGEXP = new RegExp( | |
| 213 r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$'); | |
| 214 InputEmailDirective(dom.Element inputElement, NgModel ngModel, Scope scope): | |
| 215 super(inputElement, ngModel, scope); | |
| 216 | |
| 217 String get typedValue { | |
| 218 String value = (inputElement as dom.InputElement).value; | |
| 219 return EMAIL_REGEXP.hasMatch(value) ? value : null; | |
| 220 } | |
| 221 | |
| 222 set typedValue(String value) { | |
| 223 if (value != null && EMAIL_REGEXP.hasMatch(value)) { | |
| 224 (inputElement as dom.InputElement).value = value; | |
| 225 } | |
| 226 } | |
| 227 } | |
| 228 | |
| 229 | |
| 230 /** | |
| 231 * Usage: | |
| 232 * | |
| 233 * <input type="url" ng-model="website"> | |
| 234 * | |
| 235 * This creates a two way databinding between the expression specified in | |
| 236 * ng-model and the `url` input element in the DOM. If the ng-model value is | |
| 237 * `null`, the DOM element is not updated. If the value in the DOM element is | |
| 238 * an invalid URL, then the expression specified by the `ng-model` is set to | |
| 239 * null., | |
| 240 */ | |
| 241 @NgDirective(selector: 'input[type=url][ng-model]') | |
| 242 class InputUrlDirective extends _InputTextlikeDirective { | |
| 243 static final URL_REGEXP = new RegExp( | |
| 244 r'^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?' + | |
| 245 r'(\/|\/([\w#!:.?+=&%@!\-\/]))?$'); | |
| 246 InputUrlDirective(dom.Element inputElement, NgModel ngModel, Scope scope): | |
| 247 super(inputElement, ngModel, scope); | |
| 248 | |
| 249 String get typedValue { | |
| 250 String value = (inputElement as dom.InputElement).value; | |
| 251 return URL_REGEXP.hasMatch(value) ? value : null; | |
| 252 } | |
| 253 | |
| 254 set typedValue(String value) { | |
| 255 if (value != null && URL_REGEXP.hasMatch(value)) { | |
| 256 (inputElement as dom.InputElement).value = value; | |
| 257 } | |
| 258 } | |
| 259 } | |
| 260 | |
| 261 class _UidCounter { | 225 class _UidCounter { |
| 262 static final int CHAR_0 = "0".codeUnitAt(0); | 226 static final int CHAR_0 = "0".codeUnitAt(0); |
| 263 static final int CHAR_9 = "9".codeUnitAt(0); | 227 static final int CHAR_9 = "9".codeUnitAt(0); |
| 264 static final int CHAR_A = "A".codeUnitAt(0); | 228 static final int CHAR_A = "A".codeUnitAt(0); |
| 265 static final int CHAR_Z = "Z".codeUnitAt(0); | 229 static final int CHAR_Z = "Z".codeUnitAt(0); |
| 266 List charCodes = [CHAR_0, CHAR_0, CHAR_0]; | 230 List charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
| 267 | 231 |
| 268 String next() { | 232 String next() { |
| 269 for (int i = charCodes.length-1; i >= 0; i--) { | 233 for (int i = charCodes.length-1; i >= 0; i--) { |
| 270 int code = charCodes[i]; | 234 int code = charCodes[i]; |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 318 ngModel.render = (String value) { | 282 ngModel.render = (String value) { |
| 319 radioButtonElement.checked = (value == radioButtonElement.value); | 283 radioButtonElement.checked = (value == radioButtonElement.value); |
| 320 }; | 284 }; |
| 321 radioButtonElement.onClick.listen((_) { | 285 radioButtonElement.onClick.listen((_) { |
| 322 if (radioButtonElement.checked) { | 286 if (radioButtonElement.checked) { |
| 323 scope.$apply(() => ngModel.viewValue = radioButtonElement.value); | 287 scope.$apply(() => ngModel.viewValue = radioButtonElement.value); |
| 324 } | 288 } |
| 325 }); | 289 }); |
| 326 } | 290 } |
| 327 } | 291 } |
| 292 |
| 293 /** |
| 294 * Usage (span could be replaced with any element which supports text content, s
uch as `p`): |
| 295 * |
| 296 * <span contenteditable= ng-model="name"> |
| 297 * |
| 298 * This creates a two way databinding between the expression specified in |
| 299 * ng-model and the html element in the DOM. If the ng-model value is |
| 300 * `null`, it is treated as equivalent to the empty string for rendering |
| 301 * purposes. |
| 302 */ |
| 303 @NgDirective(selector: '[contenteditable][ng-model]') |
| 304 class ContentEditableDirective extends InputTextLikeDirective { |
| 305 ContentEditableDirective(dom.Element inputElement, NgModel ngModel, Scope scop
e): |
| 306 super(inputElement, ngModel, scope); |
| 307 |
| 308 // The implementation is identical to InputTextLikeDirective but use innerHtml
instead of value |
| 309 get typedValue => (inputElement as dynamic).innerHtml; |
| 310 set typedValue(String value) => (inputElement as dynamic).innerHtml = (value =
= null) ? '' : value; |
| 311 } |
| OLD | NEW |