OLD | NEW |
1 part of angular.directive; | 1 part of angular.directive; |
2 | 2 |
3 typedef dynamic ItemEval(dynamic item, num index); | |
4 | |
5 /** | 3 /** |
6 * HTML [SELECT] element with angular data-binding if used with | 4 * HTML [SELECT] element with angular data-binding if used with |
7 * [NgModelDirective]. | 5 * [NgModel]. |
8 * | 6 * |
9 * The [NgModelDirective] will receive the currently selected item. The binding | 7 * The [NgModel] will receive the currently selected item. The binding |
10 * is performed on the [OPTION].[value] property. An empty [OPTION].[value] is | 8 * is performed on the [OPTION].[value] property. An empty [OPTION].[value] is |
11 * treated as null. | 9 * treated as null. |
12 * | 10 * |
13 * If you the model contains value which does not map to any [OPTION] then a new | 11 * If you the model contains value which does not map to any [OPTION] then a new |
14 * unknown [OPTION] is inserted into the list. Once the model points to an | 12 * unknown [OPTION] is inserted into the list. Once the model points to an |
15 * existing [OPTION] the unknown [OPTION] is removed. | 13 * existing [OPTION] the unknown [OPTION] is removed. |
16 * | 14 * |
17 * Becouse [OPTION].[value] attribute is a string, the model is bound to a | 15 * Because [OPTION].[value] attribute is a string, the model is bound to a |
18 * string. If there is need to bind to an object then [OptionValueDirective] | 16 * string. If there is need to bind to an object then [OptionValue] |
19 * should be used. | 17 * should be used. |
20 * | 18 * |
21 */ | 19 */ |
22 @NgDirective( | 20 @Decorator( |
23 selector: 'select[ng-model]', | 21 selector: 'select[ng-model]') |
24 visibility: NgDirective.CHILDREN_VISIBILITY) | 22 class InputSelect implements AttachAware { |
25 class InputSelectDirective implements NgAttachAware { | 23 final expando = new Expando<OptionValue>(); |
26 final Expando<OptionValueDirective> expando = | |
27 new Expando<OptionValueDirective>(); | |
28 final dom.SelectElement _selectElement; | 24 final dom.SelectElement _selectElement; |
29 final NodeAttrs _attrs; | 25 final NodeAttrs _attrs; |
30 final NgModel _model; | 26 final NgModel _model; |
31 final Scope _scope; | 27 final Scope _scope; |
32 | 28 |
33 final dom.OptionElement _unknownOption = new dom.OptionElement(); | 29 final dom.OptionElement _unknownOption = new dom.OptionElement(); |
34 dom.OptionElement _nullOption; | 30 dom.OptionElement _nullOption; |
35 | 31 |
36 _SelectMode _mode = new _SelectMode(null, null, null); | 32 _SelectMode _mode = new _SelectMode(null, null, null); |
37 bool _dirty = false; | 33 bool _dirty = false; |
38 | 34 |
39 InputSelectDirective(dom.Element this._selectElement, this._attrs, this._model
, | 35 InputSelect(dom.Element this._selectElement, this._attrs, this._model, |
40 this._scope) { | 36 this._scope) { |
41 _unknownOption.value = '?'; | 37 _unknownOption.value = '?'; |
42 _unknownOption.text = ''; // Explicit due to dartbug.com/14407 | 38 _nullOption = _selectElement.querySelectorAll('option') |
43 _selectElement.querySelectorAll('option').forEach((o) { | 39 .firstWhere((o) => o.value == '', orElse: () => null); |
44 if (_nullOption == null && o.value == '') { | |
45 _nullOption = o; | |
46 } | |
47 }); | |
48 } | 40 } |
49 | 41 |
50 attach() { | 42 attach() { |
51 _attrs.observe('multiple', (value) { | 43 _attrs.observe('multiple', (value) { |
52 _mode.destroy(); | 44 _mode.destroy(); |
53 if (value == null) { | 45 if (value == null) { |
54 _model.watchCollection = false; | 46 _model.watchCollection = false; |
55 _mode = new _SingleSelectMode(expando, _selectElement, _model, _nullOpti
on, _unknownOption); | 47 _mode = new _SingleSelectMode(expando, _selectElement, _model, |
| 48 _nullOption, _unknownOption); |
56 } else { | 49 } else { |
57 _model.watchCollection = true; | 50 _model.watchCollection = true; |
58 _mode = new _MultipleSelectionMode(expando, _selectElement, _model); | 51 _mode = new _MultipleSelectionMode(expando, _selectElement, _model); |
59 } | 52 } |
60 _mode.onModelChange(_model.viewValue); | 53 _mode.onModelChange(_model.viewValue); |
61 }); | 54 }); |
62 | 55 |
63 _selectElement.onChange.listen((event) => _mode.onViewChange(event)); | 56 _selectElement.onChange.listen((event) => _mode.onViewChange(event)); |
64 _model.render = (value) { | 57 _model.render = (value) { |
65 // TODO(misko): this hack need to delay the rendering until after domRead | 58 // TODO(misko): this hack need to delay the rendering until after domRead |
66 // because the modelChange reads from the DOM. We should be able to render | 59 // because the modelChange reads from the DOM. We should be able to render |
67 // without DOM changes. | 60 // without DOM changes. |
68 _scope.rootScope.domRead(() { | 61 _scope.rootScope.domRead(() { |
69 _scope.rootScope.domWrite(() => _mode.onModelChange(value)); | 62 _scope.rootScope.domWrite(() => _mode.onModelChange(value)); |
70 }); | 63 }); |
71 }; | 64 }; |
72 } | 65 } |
73 | 66 |
74 /** | 67 /** |
75 * This method invalidates the current state of the selector and forces a | 68 * This method invalidates the current state of the selector and forces a |
76 * re-rendering of the options using the [Scope.evalAsync]. | 69 * re-rendering of the options using the [Scope.evalAsync]. |
77 */ | 70 */ |
78 dirty() { | 71 dirty() { |
79 if (!_dirty) { | 72 if (!_dirty) { |
80 _dirty = true; | 73 _dirty = true; |
81 // TODO(misko): this hack need to delay the rendering until after domRead | 74 // TODO(misko): this hack need to delay the rendering until after domRead |
82 // becouse the modelChange reads from the DOM. We should be able to render | 75 // because the modelChange reads from the DOM. We should be able to render |
83 // without DOM changes. | 76 // without DOM changes. |
84 _scope.rootScope.domRead(() { | 77 _scope.rootScope.domRead(() { |
85 _scope.rootScope.domWrite(() { | 78 _scope.rootScope.domWrite(() { |
86 _dirty = false; | 79 _dirty = false; |
87 _mode.onModelChange(_model.viewValue); | 80 _mode.onModelChange(_model.viewValue); |
88 }); | 81 }); |
89 }); | 82 }); |
90 } | 83 } |
91 } | 84 } |
92 } | 85 } |
93 | 86 |
94 /** | 87 /** |
95 * Since the [value] attribute of the [OPTION] can only be a string, Angular | 88 * Since the [value] attribute of the [OPTION] can only be a string, Angular |
96 * provides [ng-value] which allows binding to any expression. | 89 * provides [ng-value] which allows binding to any expression. |
97 * | 90 * |
98 */ | 91 */ |
99 @NgDirective( | 92 @Decorator(selector: 'option', module: NgValue.moduleFactory) |
100 selector: 'option') | 93 class OptionValue implements AttachAware, |
101 class OptionValueDirective implements NgAttachAware, | 94 DetachAware { |
102 NgDetachAware { | 95 final InputSelect _inputSelectDirective; |
103 final InputSelectDirective _inputSelectDirective; | |
104 final dom.Element _element; | 96 final dom.Element _element; |
105 | 97 |
106 NgValue _ngValue; | 98 NgValue _ngValue; |
107 | 99 |
108 OptionValueDirective(this._element, this._inputSelectDirective, this._ngValue)
{ | 100 OptionValue(this._element, this._inputSelectDirective, this._ngValue) { |
109 if (_inputSelectDirective != null) { | 101 if (_inputSelectDirective != null) { |
110 _inputSelectDirective.expando[_element] = this; | 102 _inputSelectDirective.expando[_element] = this; |
111 } | 103 } |
112 } | 104 } |
113 | 105 |
114 attach() { | 106 attach() { |
115 if (_inputSelectDirective != null) { | 107 if (_inputSelectDirective != null) _inputSelectDirective.dirty(); |
116 _inputSelectDirective.dirty(); | |
117 } | |
118 } | 108 } |
119 | 109 |
120 detach() { | 110 detach() { |
121 if (_inputSelectDirective != null) { | 111 if (_inputSelectDirective != null) { |
122 _inputSelectDirective.dirty(); | 112 _inputSelectDirective.dirty(); |
123 _inputSelectDirective.expando[_element] = null; | 113 _inputSelectDirective.expando[_element] = null; |
124 } | 114 } |
125 } | 115 } |
126 | 116 |
127 get ngValue => _ngValue.readValue(_element); | 117 get ngValue => _ngValue.value; |
128 } | 118 } |
129 | 119 |
130 class _SelectMode { | 120 class _SelectMode { |
131 final Expando<OptionValueDirective> expando; | 121 final Expando<OptionValue> expando; |
132 final dom.SelectElement select; | 122 final dom.SelectElement select; |
133 final NgModel model; | 123 final NgModel model; |
134 | 124 |
135 _SelectMode(this.expando, this.select, this.model); | 125 _SelectMode(this.expando, this.select, this.model); |
136 | 126 |
137 onViewChange(event) {} | 127 onViewChange(event) {} |
138 onModelChange(value) {} | 128 onModelChange(value) {} |
139 destroy() {} | 129 destroy() {} |
140 | 130 |
141 get _options => select.querySelectorAll('option'); | 131 get _options => select.querySelectorAll('option'); |
142 _forEachOption(fn, [quiteOnReturn = false]) { | 132 _forEachOption(fn, [quiteOnReturn = false]) { |
143 for (var i = 0; i < _options.length; i++) { | 133 for (var i = 0; i < _options.length; i++) { |
144 var retValue = fn(_options[i], i); | 134 var retValue = fn(_options[i], i); |
145 if (quiteOnReturn && retValue != null) return retValue; | 135 if (quiteOnReturn && retValue != null) return retValue; |
146 } | 136 } |
147 return null; | 137 return null; |
148 } | 138 } |
149 } | 139 } |
150 | 140 |
151 class _SingleSelectMode extends _SelectMode { | 141 class _SingleSelectMode extends _SelectMode { |
152 final dom.OptionElement _unknownOption; | 142 final dom.OptionElement _unknownOption; |
153 final dom.OptionElement _nullOption; | 143 final dom.OptionElement _nullOption; |
154 | 144 |
155 bool _unknownOptionActive = false; | 145 bool _unknownOptionActive = false; |
156 | 146 |
157 _SingleSelectMode(Expando<OptionValueDirective> expando, | 147 _SingleSelectMode(Expando<OptionValue> expando, |
158 dom.SelectElement select, | 148 dom.SelectElement select, |
159 NgModel model, | 149 NgModel model, |
160 this._nullOption, | 150 this._nullOption, |
161 this._unknownOption | 151 this._unknownOption) |
162 ): super(expando, select, model) { | 152 : super(expando, select, model) { |
163 } | 153 } |
164 | 154 |
165 onViewChange(event) { | 155 onViewChange(event) { |
166 var i = 0; | 156 var i = 0; |
167 model.viewValue = _forEachOption((option, _) { | 157 model.viewValue = _forEachOption((option, _) { |
168 if (option.selected) { | 158 if (option.selected) { |
169 return option == _nullOption ? null : expando[option].ngValue; | 159 if (option == _nullOption) return null; |
| 160 assert(expando[option] != null); |
| 161 return expando[option].ngValue; |
170 } | 162 } |
171 if (option != _unknownOption && option != _nullOption) i++; | 163 if (option != _unknownOption && option != _nullOption) i++; |
172 }, true); | 164 }, true); |
173 } | 165 } |
174 | 166 |
175 onModelChange(value) { | 167 onModelChange(value) { |
176 var found = false; | 168 var found = false; |
177 _forEachOption((option, i) { | 169 _forEachOption((option, i) { |
178 if (option == _unknownOption) return; | 170 if (option == _unknownOption) return; |
179 var selected; | 171 var selected; |
180 if (value == null) { | 172 if (value == null) { |
181 selected = option == _nullOption; | 173 selected = option == _nullOption; |
182 } else { | 174 } else { |
183 OptionValueDirective optionValueDirective = expando[option]; | 175 OptionValue optionValueDirective = expando[option]; |
184 selected = optionValueDirective == null ? false : optionValueDirective.n
gValue == value; | 176 selected = optionValueDirective == null ? |
| 177 false : |
| 178 optionValueDirective.ngValue == value; |
185 } | 179 } |
186 found = found || selected; | 180 found = found || selected; |
187 option.selected = selected; | 181 option.selected = selected; |
188 }); | 182 }); |
189 | 183 |
190 if (!found) { | 184 if (!found) { |
191 if (!_unknownOptionActive) { | 185 if (!_unknownOptionActive) { |
192 select.insertBefore(_unknownOption, select.firstChild); | 186 select.insertBefore(_unknownOption, select.firstChild); |
193 _unknownOption.selected = true; | 187 _unknownOption.selected = true; |
194 _unknownOptionActive = true; | 188 _unknownOptionActive = true; |
195 } | 189 } |
196 } else { | 190 } else { |
197 if (_unknownOptionActive) { | 191 if (_unknownOptionActive) { |
198 _unknownOption.remove(); | 192 _unknownOption.remove(); |
199 _unknownOptionActive = false; | 193 _unknownOptionActive = false; |
200 } | 194 } |
201 } | 195 } |
202 } | 196 } |
203 } | 197 } |
204 | 198 |
205 class _MultipleSelectionMode extends _SelectMode { | 199 class _MultipleSelectionMode extends _SelectMode { |
206 _MultipleSelectionMode(Expando<OptionValueDirective> expando, | 200 _MultipleSelectionMode(Expando<OptionValue> expando, |
207 dom.SelectElement select, | 201 dom.SelectElement select, |
208 NgModel model) | 202 NgModel model) |
209 : super(expando, select, model); | 203 : super(expando, select, model); |
210 | 204 |
211 onViewChange(event) { | 205 onViewChange(event) { |
212 var selected = []; | 206 var selected = []; |
213 | 207 |
214 _forEachOption((o, i) { | 208 _forEachOption((o, i) { |
215 if (o.selected) selected.add(expando[o].ngValue); | 209 if (o.selected) selected.add(expando[o].ngValue); |
216 }); | 210 }); |
217 model.viewValue = selected; | 211 model.viewValue = selected; |
218 } | 212 } |
219 | 213 |
220 onModelChange(List selectedValues) { | 214 onModelChange(List selectedValues) { |
221 Function fn = (o, i) => o.selected = null; | 215 Function fn = (o, i) => o.selected = null; |
222 | 216 |
223 if (selectedValues is List) { | 217 if (selectedValues is List) { |
224 fn = (o, i) { | 218 fn = (o, i) { |
225 var selected = expando[o]; | 219 var selected = expando[o]; |
226 if (selected == null) { | 220 return selected == null ? |
227 return false; | 221 false : |
228 } else { | 222 o.selected = selectedValues.contains(selected.ngValue); |
229 return o.selected = selectedValues.contains(selected.ngValue); | |
230 } | |
231 }; | 223 }; |
232 } | 224 } |
233 | 225 |
234 _forEachOption(fn); | 226 _forEachOption(fn); |
235 } | 227 } |
236 } | 228 } |
OLD | NEW |