| OLD | NEW |
| (Empty) | |
| 1 part of angular.directive; |
| 2 |
| 3 /** |
| 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 |
| 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 |
| 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 |
| 10 * knwos how to (in)validate the model and the form in which it is declared |
| 11 * (to be implemented) |
| 12 */ |
| 13 @NgDirective( |
| 14 selector: '[ng-model]', |
| 15 map: const {'ng-model': '&model'}) |
| 16 class NgModel { |
| 17 final Scope _scope; |
| 18 |
| 19 Getter getter = ([_]) => null; |
| 20 Setter setter = (_, [__]) => null; |
| 21 String _exp; |
| 22 |
| 23 |
| 24 Function _removeWatch = () => null; |
| 25 bool _watchCollection; |
| 26 |
| 27 Function render = (value) => null; |
| 28 |
| 29 NgModel(this._scope, NodeAttrs attrs) { |
| 30 _exp = 'ng-model=${attrs["ng-model"]}'; |
| 31 watchCollection = false; |
| 32 } |
| 33 |
| 34 get watchCollection => _watchCollection; |
| 35 set watchCollection(value) { |
| 36 if (_watchCollection == value) return; |
| 37 _watchCollection = value; |
| 38 _removeWatch(); |
| 39 if (_watchCollection) { |
| 40 _removeWatch = _scope.$watchCollection((s) => getter(), (value) => render(
value), _exp); |
| 41 } else { |
| 42 _removeWatch = _scope.$watch((s) => getter(), (value) => render(value), _e
xp); |
| 43 } |
| 44 } |
| 45 |
| 46 set model(BoundExpression boundExpression) { |
| 47 getter = boundExpression; |
| 48 setter = boundExpression.assign; |
| 49 } |
| 50 |
| 51 // TODO(misko): right now viewValue and modelValue are the same, |
| 52 // but this needs to be changed to support converters and form validation |
| 53 get viewValue => modelValue; |
| 54 set viewValue(value) => modelValue = value; |
| 55 |
| 56 get modelValue => getter(); |
| 57 set modelValue(value) => setter(value); |
| 58 } |
| 59 |
| 60 /** |
| 61 * Usage: |
| 62 * |
| 63 * <input type="checkbox" ng-model="flag"> |
| 64 * |
| 65 * 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 |
| 67 * falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is unchecked. |
| 68 * Otherwise, it is checked. Likewise, when the checkbox is checked, the model |
| 69 * value is set to true. When unchecked, it is set to false. |
| 70 * |
| 71 * The AngularJS style ng-true-value / ng-false-value is not supported. |
| 72 */ |
| 73 @NgDirective(selector: 'input[type=checkbox][ng-model]') |
| 74 class InputCheckboxDirective { |
| 75 dom.InputElement inputElement; |
| 76 NgModel ngModel; |
| 77 Scope scope; |
| 78 |
| 79 InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, this.scope
) { |
| 80 ngModel.render = (value) { |
| 81 inputElement.checked = value == null ? false : toBool(value); |
| 82 }; |
| 83 inputElement.onChange.listen((value) { |
| 84 scope.$apply(() => ngModel.viewValue = inputElement.checked); |
| 85 }); |
| 86 } |
| 87 } |
| 88 |
| 89 |
| 90 abstract class _InputTextlikeDirective { |
| 91 dom.Element inputElement; |
| 92 NgModel ngModel; |
| 93 Scope scope; |
| 94 |
| 95 get typedValue => (inputElement as dynamic).value; |
| 96 set typedValue(String value) => (inputElement as dynamic).value = (value == nu
ll) ? '' : value; |
| 97 |
| 98 _InputTextlikeDirective(dom.Element this.inputElement, this.ngModel, this.scop
e) { |
| 99 ngModel.render = (value) { |
| 100 if (value == null) value = ''; |
| 101 |
| 102 var currentValue = typedValue; |
| 103 if (value != currentValue && !(value is num && currentValue is num && valu
e.isNaN && currentValue.isNaN)) { |
| 104 typedValue = value; |
| 105 } |
| 106 }; |
| 107 inputElement.onChange.listen(relaxFnArgs(processValue)); |
| 108 inputElement.onKeyDown.listen((e) { |
| 109 new async.Timer(Duration.ZERO, processValue); |
| 110 scope.$skipAutoDigest(); |
| 111 }); |
| 112 } |
| 113 |
| 114 processValue() { |
| 115 var value = typedValue; |
| 116 if (value != ngModel.viewValue) { |
| 117 scope.$apply(() => ngModel.viewValue = value); |
| 118 } |
| 119 } |
| 120 } |
| 121 |
| 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 { |
| 262 static final int CHAR_0 = "0".codeUnitAt(0); |
| 263 static final int CHAR_9 = "9".codeUnitAt(0); |
| 264 static final int CHAR_A = "A".codeUnitAt(0); |
| 265 static final int CHAR_Z = "Z".codeUnitAt(0); |
| 266 List charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
| 267 |
| 268 String next() { |
| 269 for (int i = charCodes.length-1; i >= 0; i--) { |
| 270 int code = charCodes[i]; |
| 271 if (code == CHAR_9) { |
| 272 charCodes[i] = CHAR_A; |
| 273 return new String.fromCharCodes(charCodes); |
| 274 } else if (code == CHAR_Z) { |
| 275 charCodes[i] = CHAR_0; |
| 276 } else { |
| 277 charCodes[i] = code + 1; |
| 278 return new String.fromCharCodes(charCodes); |
| 279 } |
| 280 } |
| 281 charCodes.insert(0, CHAR_0); |
| 282 return new String.fromCharCodes(charCodes); |
| 283 } |
| 284 } |
| 285 |
| 286 final _uidCounter = new _UidCounter(); |
| 287 |
| 288 |
| 289 /** |
| 290 * Usage: |
| 291 * |
| 292 * <input type="radio" ng-model="category"> |
| 293 * |
| 294 * This creates a two way databinding between the expression specified in |
| 295 * ng-model and the range input elements in the DOM. If the ng-model value is |
| 296 * set to a value not corresponding to one of the radio elements, then none of |
| 297 * the radio elements will be check. Otherwise, only the corresponding input |
| 298 * element in the group is checked. Likewise, when a radio button element is |
| 299 * checked, the model is updated with its value. Radio buttons that have a |
| 300 * `name` attribute are left alone. Those that are missing the attribute will |
| 301 * have a unique `name` assigned to them. This sequence goes `001`, `001`, ... |
| 302 * `009`, `00A`, `00Z`, `010`, … and so on using more than 3 characters for the |
| 303 * name when the counter overflows. |
| 304 */ |
| 305 @NgDirective(selector: 'input[type=radio][ng-model]') |
| 306 class InputRadioDirective { |
| 307 dom.RadioButtonInputElement radioButtonElement; |
| 308 NgModel ngModel; |
| 309 Scope scope; |
| 310 |
| 311 InputRadioDirective(dom.Element this.radioButtonElement, this.ngModel, |
| 312 this.scope, NodeAttrs attrs) { |
| 313 // If there's no "name" set, we'll set a unique name. This ensures |
| 314 // less surprising behavior about which radio buttons are grouped together. |
| 315 if (attrs['name'] == '' || attrs['name'] == null) { |
| 316 attrs["name"] = _uidCounter.next(); |
| 317 } |
| 318 ngModel.render = (String value) { |
| 319 radioButtonElement.checked = (value == radioButtonElement.value); |
| 320 }; |
| 321 radioButtonElement.onClick.listen((_) { |
| 322 if (radioButtonElement.checked) { |
| 323 scope.$apply(() => ngModel.viewValue = radioButtonElement.value); |
| 324 } |
| 325 }); |
| 326 } |
| 327 } |
| OLD | NEW |