@angular-wave/angular.ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.cjs +29 -0
  3. package/.github/workflows/playwright.yml +27 -0
  4. package/CHANGELOG.md +17974 -0
  5. package/CODE_OF_CONDUCT.md +3 -0
  6. package/CONTRIBUTING.md +246 -0
  7. package/DEVELOPERS.md +488 -0
  8. package/LICENSE +22 -0
  9. package/Makefile +31 -0
  10. package/README.md +115 -0
  11. package/RELEASE.md +98 -0
  12. package/SECURITY.md +16 -0
  13. package/TRIAGING.md +135 -0
  14. package/css/angular.css +22 -0
  15. package/dist/angular-ts.cjs.js +36843 -0
  16. package/dist/angular-ts.esm.js +36841 -0
  17. package/dist/angular-ts.umd.js +36848 -0
  18. package/dist/build/angular-animate.js +4272 -0
  19. package/dist/build/angular-aria.js +426 -0
  20. package/dist/build/angular-message-format.js +1072 -0
  21. package/dist/build/angular-messages.js +829 -0
  22. package/dist/build/angular-mocks.js +3757 -0
  23. package/dist/build/angular-parse-ext.js +1275 -0
  24. package/dist/build/angular-resource.js +911 -0
  25. package/dist/build/angular-route.js +1266 -0
  26. package/dist/build/angular-sanitize.js +891 -0
  27. package/dist/build/angular-touch.js +368 -0
  28. package/dist/build/angular.js +36600 -0
  29. package/e2e/unit.spec.ts +15 -0
  30. package/images/android-chrome-192x192.png +0 -0
  31. package/images/android-chrome-512x512.png +0 -0
  32. package/images/apple-touch-icon.png +0 -0
  33. package/images/favicon-16x16.png +0 -0
  34. package/images/favicon-32x32.png +0 -0
  35. package/images/favicon.ico +0 -0
  36. package/images/site.webmanifest +1 -0
  37. package/index.html +104 -0
  38. package/package.json +47 -0
  39. package/playwright.config.ts +78 -0
  40. package/public/circle.html +1 -0
  41. package/public/my_child_directive.html +1 -0
  42. package/public/my_directive.html +1 -0
  43. package/public/my_other_directive.html +1 -0
  44. package/public/test.html +1 -0
  45. package/rollup.config.js +31 -0
  46. package/src/animations/animateCache.js +55 -0
  47. package/src/animations/animateChildrenDirective.js +105 -0
  48. package/src/animations/animateCss.js +1139 -0
  49. package/src/animations/animateCssDriver.js +291 -0
  50. package/src/animations/animateJs.js +367 -0
  51. package/src/animations/animateJsDriver.js +67 -0
  52. package/src/animations/animateQueue.js +851 -0
  53. package/src/animations/animation.js +506 -0
  54. package/src/animations/module.js +779 -0
  55. package/src/animations/ngAnimateSwap.js +119 -0
  56. package/src/animations/rafScheduler.js +50 -0
  57. package/src/animations/shared.js +378 -0
  58. package/src/constants.js +20 -0
  59. package/src/core/animate.js +845 -0
  60. package/src/core/animateCss.js +73 -0
  61. package/src/core/animateRunner.js +195 -0
  62. package/src/core/attributes.js +199 -0
  63. package/src/core/cache.js +45 -0
  64. package/src/core/compile.js +4727 -0
  65. package/src/core/controller.js +225 -0
  66. package/src/core/exceptionHandler.js +63 -0
  67. package/src/core/filter.js +146 -0
  68. package/src/core/interpolate.js +442 -0
  69. package/src/core/interval.js +188 -0
  70. package/src/core/intervalFactory.js +57 -0
  71. package/src/core/location.js +1086 -0
  72. package/src/core/parser/parse.js +2562 -0
  73. package/src/core/parser/parse.md +13 -0
  74. package/src/core/q.js +746 -0
  75. package/src/core/rootScope.js +1596 -0
  76. package/src/core/sanitizeUri.js +85 -0
  77. package/src/core/sce.js +1161 -0
  78. package/src/core/taskTrackerFactory.js +125 -0
  79. package/src/core/timeout.js +121 -0
  80. package/src/core/urlUtils.js +187 -0
  81. package/src/core/utils.js +1349 -0
  82. package/src/directive/a.js +37 -0
  83. package/src/directive/attrs.js +283 -0
  84. package/src/directive/bind.js +51 -0
  85. package/src/directive/bind.md +142 -0
  86. package/src/directive/change.js +12 -0
  87. package/src/directive/change.md +25 -0
  88. package/src/directive/cloak.js +12 -0
  89. package/src/directive/cloak.md +24 -0
  90. package/src/directive/events.js +75 -0
  91. package/src/directive/events.md +166 -0
  92. package/src/directive/form.js +725 -0
  93. package/src/directive/init.js +15 -0
  94. package/src/directive/init.md +41 -0
  95. package/src/directive/input.js +1783 -0
  96. package/src/directive/list.js +46 -0
  97. package/src/directive/list.md +22 -0
  98. package/src/directive/ngClass.js +249 -0
  99. package/src/directive/ngController.js +64 -0
  100. package/src/directive/ngCsp.js +82 -0
  101. package/src/directive/ngIf.js +134 -0
  102. package/src/directive/ngInclude.js +217 -0
  103. package/src/directive/ngModel.js +1356 -0
  104. package/src/directive/ngModelOptions.js +509 -0
  105. package/src/directive/ngOptions.js +670 -0
  106. package/src/directive/ngRef.js +90 -0
  107. package/src/directive/ngRepeat.js +650 -0
  108. package/src/directive/ngShowHide.js +255 -0
  109. package/src/directive/ngSwitch.js +178 -0
  110. package/src/directive/ngTransclude.js +98 -0
  111. package/src/directive/non-bindable.js +11 -0
  112. package/src/directive/non-bindable.md +17 -0
  113. package/src/directive/script.js +30 -0
  114. package/src/directive/select.js +624 -0
  115. package/src/directive/style.js +25 -0
  116. package/src/directive/style.md +23 -0
  117. package/src/directive/validators.js +329 -0
  118. package/src/exts/aria.js +544 -0
  119. package/src/exts/messages.js +852 -0
  120. package/src/filters/filter.js +207 -0
  121. package/src/filters/filter.md +69 -0
  122. package/src/filters/filters.js +239 -0
  123. package/src/filters/json.md +16 -0
  124. package/src/filters/limit-to.js +43 -0
  125. package/src/filters/limit-to.md +19 -0
  126. package/src/filters/order-by.js +183 -0
  127. package/src/filters/order-by.md +83 -0
  128. package/src/index.js +13 -0
  129. package/src/injector.js +1034 -0
  130. package/src/jqLite.js +1117 -0
  131. package/src/loader.js +1320 -0
  132. package/src/public.js +215 -0
  133. package/src/routeToRegExp.js +41 -0
  134. package/src/services/anchorScroll.js +135 -0
  135. package/src/services/browser.js +321 -0
  136. package/src/services/cacheFactory.js +398 -0
  137. package/src/services/cookieReader.js +72 -0
  138. package/src/services/document.js +64 -0
  139. package/src/services/http.js +1537 -0
  140. package/src/services/httpBackend.js +206 -0
  141. package/src/services/log.js +160 -0
  142. package/src/services/templateRequest.js +139 -0
  143. package/test/angular.spec.js +2153 -0
  144. package/test/aria/aria.spec.js +1245 -0
  145. package/test/binding.spec.js +504 -0
  146. package/test/build-test.html +14 -0
  147. package/test/injector.spec.js +2327 -0
  148. package/test/jasmine/jasmine-5.1.2/boot0.js +65 -0
  149. package/test/jasmine/jasmine-5.1.2/boot1.js +133 -0
  150. package/test/jasmine/jasmine-5.1.2/jasmine-html.js +963 -0
  151. package/test/jasmine/jasmine-5.1.2/jasmine.css +320 -0
  152. package/test/jasmine/jasmine-5.1.2/jasmine.js +10824 -0
  153. package/test/jasmine/jasmine-5.1.2/jasmine_favicon.png +0 -0
  154. package/test/jasmine/jasmine-browser.json +17 -0
  155. package/test/jasmine/jasmine.json +9 -0
  156. package/test/jqlite.spec.js +2133 -0
  157. package/test/loader.spec.js +219 -0
  158. package/test/messages/messages.spec.js +1146 -0
  159. package/test/min-err.spec.js +174 -0
  160. package/test/mock-test.html +13 -0
  161. package/test/module-test.html +15 -0
  162. package/test/ng/anomate.spec.js +606 -0
  163. package/test/ng/cache-factor.spec.js +334 -0
  164. package/test/ng/compile.spec.js +17956 -0
  165. package/test/ng/controller-provider.spec.js +227 -0
  166. package/test/ng/cookie-reader.spec.js +98 -0
  167. package/test/ng/directive/a.spec.js +192 -0
  168. package/test/ng/directive/bind.spec.js +334 -0
  169. package/test/ng/directive/boolean.spec.js +136 -0
  170. package/test/ng/directive/change.spec.js +71 -0
  171. package/test/ng/directive/class.spec.js +858 -0
  172. package/test/ng/directive/click.spec.js +38 -0
  173. package/test/ng/directive/cloak.spec.js +44 -0
  174. package/test/ng/directive/constoller.spec.js +194 -0
  175. package/test/ng/directive/element-style.spec.js +92 -0
  176. package/test/ng/directive/event.spec.js +282 -0
  177. package/test/ng/directive/form.spec.js +1518 -0
  178. package/test/ng/directive/href.spec.js +143 -0
  179. package/test/ng/directive/if.spec.js +402 -0
  180. package/test/ng/directive/include.spec.js +828 -0
  181. package/test/ng/directive/init.spec.js +68 -0
  182. package/test/ng/directive/input.spec.js +3810 -0
  183. package/test/ng/directive/list.spec.js +170 -0
  184. package/test/ng/directive/model-options.spec.js +1008 -0
  185. package/test/ng/directive/model.spec.js +1905 -0
  186. package/test/ng/directive/non-bindable.spec.js +55 -0
  187. package/test/ng/directive/options.spec.js +3583 -0
  188. package/test/ng/directive/ref.spec.js +575 -0
  189. package/test/ng/directive/repeat.spec.js +1675 -0
  190. package/test/ng/directive/script.spec.js +52 -0
  191. package/test/ng/directive/scrset.spec.js +67 -0
  192. package/test/ng/directive/select.spec.js +2541 -0
  193. package/test/ng/directive/show-hide.spec.js +253 -0
  194. package/test/ng/directive/src.spec.js +157 -0
  195. package/test/ng/directive/style.spec.js +178 -0
  196. package/test/ng/directive/switch.spec.js +647 -0
  197. package/test/ng/directive/validators.spec.js +717 -0
  198. package/test/ng/document.spec.js +52 -0
  199. package/test/ng/filter/filter.spec.js +714 -0
  200. package/test/ng/filter/filters.spec.js +35 -0
  201. package/test/ng/filter/limit-to.spec.js +251 -0
  202. package/test/ng/filter/order-by.spec.js +891 -0
  203. package/test/ng/filter.spec.js +149 -0
  204. package/test/ng/http-backend.spec.js +398 -0
  205. package/test/ng/http.spec.js +4071 -0
  206. package/test/ng/interpolate.spec.js +642 -0
  207. package/test/ng/interval.spec.js +343 -0
  208. package/test/ng/location.spec.js +3488 -0
  209. package/test/ng/on.spec.js +229 -0
  210. package/test/ng/parse.spec.js +4655 -0
  211. package/test/ng/prop.spec.js +805 -0
  212. package/test/ng/q.spec.js +2904 -0
  213. package/test/ng/root-element.spec.js +16 -0
  214. package/test/ng/sanitize-uri.spec.js +249 -0
  215. package/test/ng/sce.spec.js +660 -0
  216. package/test/ng/scope.spec.js +3442 -0
  217. package/test/ng/template-request.spec.js +236 -0
  218. package/test/ng/timeout.spec.js +351 -0
  219. package/test/ng/url-utils.spec.js +156 -0
  220. package/test/ng/utils.spec.js +144 -0
  221. package/test/original-test.html +21 -0
  222. package/test/public.spec.js +34 -0
  223. package/test/sanitize/bing-html.spec.js +36 -0
  224. package/test/server/express.js +158 -0
  225. package/test/test-utils.js +11 -0
  226. package/tsconfig.json +17 -0
  227. package/types/angular.d.ts +138 -0
  228. package/types/global.d.ts +9 -0
  229. package/types/index.d.ts +2357 -0
  230. package/types/jqlite.d.ts +558 -0
  231. package/vite.config.js +14 -0
@@ -0,0 +1,3583 @@
1
+ import { createInjector } from "../../../src/injector";
2
+ import { jqLite, dealoc } from "../../../src/jqLite";
3
+ import { publishExternalAPI } from "../../../src/public";
4
+ import {
5
+ forEach,
6
+ isBoolean,
7
+ hashKey,
8
+ equals,
9
+ isString,
10
+ isFunction,
11
+ } from "../../../src/core/utils";
12
+ import { browserTrigger } from "../../test-utils";
13
+
14
+ describe("ngOptions", () => {
15
+ let scope;
16
+ let formElement;
17
+ let element;
18
+ let $compile;
19
+ let linkLog;
20
+ let childListMutationObserver;
21
+ let ngModelCtrl;
22
+ let injector;
23
+
24
+ function compile(html) {
25
+ formElement = jqLite(`<form name="form">${html}</form>`);
26
+ element = formElement.find("select");
27
+ $compile(formElement)(scope);
28
+ ngModelCtrl = element.controller("ngModel");
29
+ scope.$apply();
30
+ }
31
+
32
+ function setSelectValue(selectElement, optionIndex) {
33
+ const option = selectElement.find("option").eq(optionIndex);
34
+ selectElement.val(option.val());
35
+ browserTrigger(element, "change");
36
+ }
37
+
38
+ beforeEach(() => {
39
+ jasmine.addMatchers({
40
+ toEqualSelectValue() {
41
+ return {
42
+ compare(_actual_, value, multiple) {
43
+ const errors = [];
44
+ let actual = _actual_.val();
45
+
46
+ if (multiple) {
47
+ value = value.map((val) => hashKey(val));
48
+ actual = actual || [];
49
+ } else {
50
+ value = hashKey(value);
51
+ }
52
+
53
+ if (!equals(actual, value)) {
54
+ errors.push(
55
+ `Expected select value "${actual}" to equal "${value}"`,
56
+ );
57
+ }
58
+ const message = function () {
59
+ return errors.join("\n");
60
+ };
61
+
62
+ return { pass: errors.length === 0, message };
63
+ },
64
+ };
65
+ },
66
+ toEqualOption() {
67
+ return {
68
+ compare(actual, value, text, label) {
69
+ const errors = [];
70
+ const hash = hashKey(value);
71
+ if (actual.attr("value") !== hash) {
72
+ errors.push(
73
+ `Expected option value "${actual.attr("value")}" to equal "${hash}"`,
74
+ );
75
+ }
76
+ if (text && actual.text() !== text) {
77
+ errors.push(
78
+ `Expected option text "${actual.text()}" to equal "${text}"`,
79
+ );
80
+ }
81
+ if (label && actual.attr("label") !== label) {
82
+ errors.push(
83
+ `Expected option label "${actual.attr("label")}" to equal "${label}"`,
84
+ );
85
+ }
86
+
87
+ const message = function () {
88
+ return errors.join("\n");
89
+ };
90
+
91
+ return { pass: errors.length === 0, message };
92
+ },
93
+ };
94
+ },
95
+ toEqualTrackedOption() {
96
+ return {
97
+ compare(actual, value, text, label) {
98
+ const errors = [];
99
+ if (actual.attr("value") !== `${value}`) {
100
+ errors.push(
101
+ `Expected option value "${actual.attr("value")}" to equal "${value}"`,
102
+ );
103
+ }
104
+ if (text && actual.text() !== text) {
105
+ errors.push(
106
+ `Expected option text "${actual.text()}" to equal "${text}"`,
107
+ );
108
+ }
109
+ if (label && actual.attr("label") !== label) {
110
+ errors.push(
111
+ `Expected option label "${actual.attr("label")}" to equal "${label}"`,
112
+ );
113
+ }
114
+
115
+ const message = function () {
116
+ return errors.join("\n");
117
+ };
118
+
119
+ return { pass: errors.length === 0, message };
120
+ },
121
+ };
122
+ },
123
+ toEqualUnknownOption() {
124
+ return {
125
+ compare(actual) {
126
+ const errors = [];
127
+ if (actual.attr("value") !== "?") {
128
+ errors.push(
129
+ `Expected option value "${actual.attr("value")}" to equal "?"`,
130
+ );
131
+ }
132
+
133
+ const message = function () {
134
+ return errors.join("\n");
135
+ };
136
+
137
+ return { pass: errors.length === 0, message };
138
+ },
139
+ };
140
+ },
141
+ toEqualUnknownValue() {
142
+ return {
143
+ compare(actual, value) {
144
+ const errors = [];
145
+ if (actual !== "?") {
146
+ errors.push(`Expected select value "${actual}" to equal "?"`);
147
+ }
148
+
149
+ const message = function () {
150
+ return errors.join("\n");
151
+ };
152
+
153
+ return { pass: errors.length === 0, message };
154
+ },
155
+ };
156
+ },
157
+ });
158
+ });
159
+
160
+ beforeEach(() => {
161
+ publishExternalAPI().decorator("$exceptionHandler", function () {
162
+ return (exception, cause) => {
163
+ throw new Error(exception.message);
164
+ };
165
+ });
166
+ injector = createInjector([
167
+ "ng",
168
+ ($compileProvider, $provide) => {
169
+ linkLog = [];
170
+
171
+ $compileProvider
172
+ .directive("customSelect", () => ({
173
+ restrict: "E",
174
+ replace: true,
175
+ scope: {
176
+ ngModel: "=",
177
+ options: "=",
178
+ },
179
+ templateUrl: "select_template.html",
180
+ link(scope, $element, attributes) {
181
+ scope.selectable_options = scope.options;
182
+ },
183
+ }))
184
+
185
+ .directive("oCompileContents", () => ({
186
+ link(scope, element) {
187
+ linkLog.push("linkCompileContents");
188
+ $compile(jqLite(element[0].childNodes))(scope);
189
+ },
190
+ }))
191
+
192
+ .directive("observeChildList", () => ({
193
+ link(scope, element) {
194
+ const config = { childList: true };
195
+
196
+ childListMutationObserver = new window.MutationObserver(() => {});
197
+ childListMutationObserver.observe(element[0], config);
198
+ },
199
+ }));
200
+
201
+ $provide.decorator("ngOptionsDirective", ($delegate) => {
202
+ const origPreLink = $delegate[0].link.pre;
203
+ const origPostLink = $delegate[0].link.post;
204
+
205
+ $delegate[0].compile = function () {
206
+ return {
207
+ pre: origPreLink,
208
+ post() {
209
+ linkLog.push("linkNgOptions");
210
+ origPostLink.apply(this, arguments);
211
+ },
212
+ };
213
+ };
214
+
215
+ return $delegate;
216
+ });
217
+ },
218
+ ]);
219
+ $compile = injector.get("$compile");
220
+ scope = injector.get("$rootScope").$new(); // create a child scope because the root scope can't be $destroy-ed
221
+ formElement = element = null;
222
+ });
223
+
224
+ afterEach(() => {
225
+ scope.$destroy(); // disables unknown option work during destruction
226
+ dealoc(formElement);
227
+ ngModelCtrl = null;
228
+ });
229
+
230
+ function createSelect(attrs, blank, unknown) {
231
+ let html = "<select";
232
+ forEach(attrs, (value, key) => {
233
+ if (isBoolean(value)) {
234
+ if (value) html += ` ${key}`;
235
+ } else {
236
+ html += ` ${key}="${value}"`;
237
+ }
238
+ });
239
+ html += `>${
240
+ blank ? (isString(blank) ? blank : '<option value="">blank</option>') : ""
241
+ }${
242
+ unknown
243
+ ? isString(unknown)
244
+ ? unknown
245
+ : '<option value="?">unknown</option>'
246
+ : ""
247
+ }</select>`;
248
+
249
+ compile(html);
250
+ }
251
+
252
+ function createSingleSelect(blank, unknown) {
253
+ createSelect(
254
+ {
255
+ "ng-model": "selected",
256
+ "ng-options": "value.name for value in values",
257
+ },
258
+ blank,
259
+ unknown,
260
+ );
261
+ }
262
+
263
+ function createMultiSelect(blank, unknown) {
264
+ createSelect(
265
+ {
266
+ "ng-model": "selected",
267
+ multiple: true,
268
+ "ng-options": "value.name for value in values",
269
+ },
270
+ blank,
271
+ unknown,
272
+ );
273
+ }
274
+
275
+ it('should throw when not formated "? for ? in ?"', () => {
276
+ expect(() => {
277
+ compile(
278
+ '<select ng-model="selected" ng-options="i dont parse"></select>',
279
+ );
280
+ }).toThrowError(/iexp/);
281
+ });
282
+
283
+ it("should have a dependency on ngModel", () => {
284
+ expect(() => {
285
+ compile('<select ng-options="item in items"></select>');
286
+ }).toThrow();
287
+ });
288
+
289
+ it("should render a list", () => {
290
+ createSingleSelect();
291
+
292
+ scope.$apply(() => {
293
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
294
+ scope.selected = scope.values[1];
295
+ });
296
+
297
+ const options = element.find("option");
298
+ expect(options.length).toEqual(3);
299
+ expect(options.eq(0)).toEqualOption(scope.values[0], "A");
300
+ expect(options.eq(1)).toEqualOption(scope.values[1], "B");
301
+ expect(options.eq(2)).toEqualOption(scope.values[2], "C");
302
+ expect(options[1].selected).toEqual(true);
303
+ });
304
+
305
+ it("should not include properties with non-numeric keys in array-like collections when using array syntax", () => {
306
+ createSelect({
307
+ "ng-model": "selected",
308
+ "ng-options": "value for value in values",
309
+ });
310
+
311
+ scope.$apply(() => {
312
+ scope.values = { 0: "X", 1: "Y", 2: "Z", a: "A", length: 3 };
313
+ scope.selected = scope.values[1];
314
+ });
315
+
316
+ const options = element.find("option");
317
+ expect(options.length).toEqual(3);
318
+ expect(options.eq(0)).toEqualOption("X");
319
+ expect(options.eq(1)).toEqualOption("Y");
320
+ expect(options.eq(2)).toEqualOption("Z");
321
+ });
322
+
323
+ it("should include properties with non-numeric keys in array-like collections when using object syntax", () => {
324
+ createSelect({
325
+ "ng-model": "selected",
326
+ "ng-options": "value for (key, value) in values",
327
+ });
328
+
329
+ scope.$apply(() => {
330
+ scope.values = { 0: "X", 1: "Y", 2: "Z", a: "A", length: 3 };
331
+ scope.selected = scope.values[1];
332
+ });
333
+
334
+ const options = element.find("option");
335
+ expect(options.length).toEqual(5);
336
+ expect(options.eq(0)).toEqualOption("X");
337
+ expect(options.eq(1)).toEqualOption("Y");
338
+ expect(options.eq(2)).toEqualOption("Z");
339
+ expect(options.eq(3)).toEqualOption("A");
340
+ expect(options.eq(4)).toEqualOption(3);
341
+ });
342
+
343
+ it("should render an object", () => {
344
+ createSelect({
345
+ "ng-model": "selected",
346
+ "ng-options": "value as key for (key, value) in object",
347
+ });
348
+
349
+ scope.$apply(() => {
350
+ scope.object = { red: "FF0000", green: "00FF00", blue: "0000FF" };
351
+ scope.selected = scope.object.green;
352
+ });
353
+
354
+ let options = element.find("option");
355
+ expect(options.length).toEqual(3);
356
+ expect(options.eq(0)).toEqualOption("FF0000", "red");
357
+ expect(options.eq(1)).toEqualOption("00FF00", "green");
358
+ expect(options.eq(2)).toEqualOption("0000FF", "blue");
359
+ expect(options[1].selected).toEqual(true);
360
+ scope.$apply('object.azur = "8888FF"');
361
+
362
+ options = element.find("option");
363
+ expect(options[1].selected).toEqual(true);
364
+
365
+ scope.$apply("selected = object.azur");
366
+
367
+ options = element.find("option");
368
+ expect(options[3].selected).toEqual(true);
369
+ });
370
+
371
+ it('should set the "selected" attribute and property on selected options', () => {
372
+ scope.values = [
373
+ {
374
+ id: "FF0000",
375
+ display: "red",
376
+ },
377
+ {
378
+ id: "0000FF",
379
+ display: "blue",
380
+ },
381
+ ];
382
+ scope.selected = "FF0000";
383
+
384
+ createSelect({
385
+ "ng-model": "selected",
386
+ "ng-options": "option.id as option.display for option in values",
387
+ });
388
+ scope.$digest();
389
+
390
+ const options = element.find("option");
391
+ expect(options.length).toEqual(2);
392
+ expect(options.eq(0)).toEqualOption("FF0000", "red");
393
+ expect(options.eq(1)).toEqualOption("0000FF", "blue");
394
+
395
+ expect(options.eq(0)[0].getAttribute("selected")).toBe("selected");
396
+ expect(options.eq(0).attr("selected")).toBe("selected");
397
+ expect(options.eq(0)[0].selected).toBe(true);
398
+ expect(options.eq(0).prop("selected")).toBe(true);
399
+
400
+ scope.selected = "0000FF";
401
+ scope.$digest();
402
+
403
+ expect(options.eq(1)[0].getAttribute("selected")).toBe("selected");
404
+ expect(options.eq(1).attr("selected")).toBe("selected");
405
+ expect(options.eq(1)[0].selected).toBe(true);
406
+ expect(options.eq(1).prop("selected")).toBe(true);
407
+ });
408
+
409
+ it("should render zero as a valid display value", () => {
410
+ createSingleSelect();
411
+
412
+ scope.$apply(() => {
413
+ scope.values = [{ name: 0 }, { name: 1 }, { name: 2 }];
414
+ scope.selected = scope.values[0];
415
+ });
416
+
417
+ const options = element.find("option");
418
+ expect(options.length).toEqual(3);
419
+ expect(options.eq(0)).toEqualOption(scope.values[0], "0");
420
+ expect(options.eq(1)).toEqualOption(scope.values[1], "1");
421
+ expect(options.eq(2)).toEqualOption(scope.values[2], "2");
422
+ });
423
+
424
+ it("should not be set when an option is selected and options are set asynchronously", (done) => {
425
+ element = $compile(
426
+ '<select ng-model="model" ng-options="opt.id as opt.label for opt in options">' +
427
+ "</select>",
428
+ )(scope);
429
+
430
+ scope.$apply(() => {
431
+ scope.model = 0;
432
+ });
433
+
434
+ setTimeout(() => {
435
+ scope.options = [
436
+ { id: 0, label: "x" },
437
+ { id: 1, label: "y" },
438
+ ];
439
+ scope.$digest();
440
+ const options = element.find("option");
441
+ expect(options.length).toEqual(2);
442
+ expect(options.eq(0)).toEqualOption(0, "x");
443
+ expect(options.eq(1)).toEqualOption(1, "y");
444
+ done();
445
+ }, 0);
446
+ });
447
+
448
+ it("should grow list", () => {
449
+ createSingleSelect();
450
+
451
+ scope.$apply(() => {
452
+ scope.values = [];
453
+ });
454
+
455
+ expect(element.find("option").length).toEqual(1); // because we add special unknown option
456
+ expect(element.find("option").eq(0)).toEqualUnknownOption();
457
+
458
+ scope.$apply(() => {
459
+ scope.values.push({ name: "A" });
460
+ scope.selected = scope.values[0];
461
+ });
462
+
463
+ expect(element.find("option").length).toEqual(1);
464
+ expect(element.find("option")).toEqualOption(scope.values[0], "A");
465
+
466
+ scope.$apply(() => {
467
+ scope.values.push({ name: "B" });
468
+ });
469
+
470
+ expect(element.find("option").length).toEqual(2);
471
+ expect(element.find("option").eq(0)).toEqualOption(scope.values[0], "A");
472
+ expect(element.find("option").eq(1)).toEqualOption(scope.values[1], "B");
473
+ });
474
+
475
+ it("should shrink list", () => {
476
+ createSingleSelect();
477
+
478
+ scope.$apply(() => {
479
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
480
+ scope.selected = scope.values[0];
481
+ });
482
+
483
+ expect(element.find("option").length).toEqual(3);
484
+
485
+ scope.$apply(() => {
486
+ scope.values.pop();
487
+ });
488
+
489
+ expect(element.find("option").length).toEqual(2);
490
+ expect(element.find("option").eq(0)).toEqualOption(scope.values[0], "A");
491
+ expect(element.find("option").eq(1)).toEqualOption(scope.values[1], "B");
492
+
493
+ scope.$apply(() => {
494
+ scope.values.pop();
495
+ });
496
+
497
+ expect(element.find("option").length).toEqual(1);
498
+ expect(element.find("option")).toEqualOption(scope.values[0], "A");
499
+
500
+ scope.$apply(() => {
501
+ scope.values.pop();
502
+ scope.selected = null;
503
+ });
504
+
505
+ expect(element.find("option").length).toEqual(1); // we add back the special empty option
506
+ });
507
+
508
+ it("should shrink and then grow list", () => {
509
+ createSingleSelect();
510
+
511
+ scope.$apply(() => {
512
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
513
+ scope.selected = scope.values[0];
514
+ });
515
+
516
+ expect(element.find("option").length).toEqual(3);
517
+
518
+ scope.$apply(() => {
519
+ scope.values = [{ name: "1" }, { name: "2" }];
520
+ scope.selected = scope.values[0];
521
+ });
522
+
523
+ expect(element.find("option").length).toEqual(2);
524
+
525
+ scope.$apply(() => {
526
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
527
+ scope.selected = scope.values[0];
528
+ });
529
+
530
+ expect(element.find("option").length).toEqual(3);
531
+ });
532
+
533
+ it("should update list", () => {
534
+ createSingleSelect();
535
+
536
+ scope.$apply(() => {
537
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
538
+ scope.selected = scope.values[0];
539
+ });
540
+
541
+ scope.$apply(() => {
542
+ scope.values = [{ name: "B" }, { name: "C" }, { name: "D" }];
543
+ scope.selected = scope.values[0];
544
+ });
545
+
546
+ const options = element.find("option");
547
+ expect(options.length).toEqual(3);
548
+ expect(options.eq(0)).toEqualOption(scope.values[0], "B");
549
+ expect(options.eq(1)).toEqualOption(scope.values[1], "C");
550
+ expect(options.eq(2)).toEqualOption(scope.values[2], "D");
551
+ });
552
+
553
+ it("should preserve pre-existing empty option", () => {
554
+ createSingleSelect(true);
555
+
556
+ scope.$apply(() => {
557
+ scope.values = [];
558
+ });
559
+ expect(element.find("option").length).toEqual(1);
560
+
561
+ scope.$apply(() => {
562
+ scope.values = [{ name: "A" }];
563
+ scope.selected = scope.values[0];
564
+ });
565
+
566
+ expect(element.find("option").length).toEqual(2);
567
+ expect(jqLite(element.find("option")[0]).text()).toEqual("blank");
568
+ expect(jqLite(element.find("option")[1]).text()).toEqual("A");
569
+
570
+ scope.$apply(() => {
571
+ scope.values = [];
572
+ scope.selected = null;
573
+ });
574
+
575
+ expect(element.find("option").length).toEqual(1);
576
+ expect(jqLite(element.find("option")[0]).text()).toEqual("blank");
577
+ });
578
+
579
+ it("should ignore $ and $$ properties", () => {
580
+ createSelect({
581
+ "ng-options": "key as value for (key, value) in object",
582
+ "ng-model": "selected",
583
+ });
584
+
585
+ scope.$apply(() => {
586
+ scope.object = {
587
+ regularProperty: "visible",
588
+ $$private: "invisible",
589
+ $property: "invisible",
590
+ };
591
+ scope.selected = "regularProperty";
592
+ });
593
+
594
+ const options = element.find("option");
595
+ expect(options.length).toEqual(1);
596
+ expect(options.eq(0)).toEqualOption("regularProperty", "visible");
597
+ });
598
+
599
+ it("should not watch non-numeric array properties", () => {
600
+ createSelect({
601
+ "ng-options": "value as createLabel(value) for value in array",
602
+ "ng-model": "selected",
603
+ });
604
+ scope.createLabel = jasmine
605
+ .createSpy("createLabel")
606
+ .and.callFake((value) => value);
607
+ scope.array = ["a", "b", "c"];
608
+ scope.array.$$private = "do not watch";
609
+ scope.array.$property = "do not watch";
610
+ scope.array.other = "do not watch";
611
+ scope.array.fn = function () {};
612
+ scope.selected = "b";
613
+ scope.$digest();
614
+
615
+ expect(scope.createLabel).toHaveBeenCalledWith("a");
616
+ expect(scope.createLabel).toHaveBeenCalledWith("b");
617
+ expect(scope.createLabel).toHaveBeenCalledWith("c");
618
+ expect(scope.createLabel).not.toHaveBeenCalledWith("do not watch");
619
+ expect(scope.createLabel).not.toHaveBeenCalledWith(jasmine.any(Function));
620
+ });
621
+
622
+ it("should not watch object properties that start with $ or $$", () => {
623
+ createSelect({
624
+ "ng-options": "key as createLabel(key) for (key, value) in object",
625
+ "ng-model": "selected",
626
+ });
627
+ scope.createLabel = jasmine
628
+ .createSpy("createLabel")
629
+ .and.callFake((value) => value);
630
+ scope.object = {
631
+ regularProperty: "visible",
632
+ $$private: "invisible",
633
+ $property: "invisible",
634
+ };
635
+ scope.selected = "regularProperty";
636
+ scope.$digest();
637
+
638
+ expect(scope.createLabel).toHaveBeenCalledWith("regularProperty");
639
+ expect(scope.createLabel).not.toHaveBeenCalledWith("$$private");
640
+ expect(scope.createLabel).not.toHaveBeenCalledWith("$property");
641
+ });
642
+
643
+ it("should allow expressions over multiple lines", () => {
644
+ scope.isNotFoo = function (item) {
645
+ return item.name !== "Foo";
646
+ };
647
+
648
+ createSelect({
649
+ "ng-options": "key.id\n" + "for key in values\n" + "| filter:isNotFoo",
650
+ "ng-model": "selected",
651
+ });
652
+
653
+ scope.$apply(() => {
654
+ scope.values = [
655
+ { id: 1, name: "Foo" },
656
+ { id: 2, name: "Bar" },
657
+ { id: 3, name: "Baz" },
658
+ ];
659
+ scope.selected = scope.values[0];
660
+ });
661
+
662
+ const options = element.find("option");
663
+ expect(options.length).toEqual(3);
664
+ expect(options.eq(1)).toEqualOption(scope.values[1], "2");
665
+ expect(options.eq(2)).toEqualOption(scope.values[2], "3");
666
+ });
667
+
668
+ it("should not update selected property of an option element on digest with no change event", () => {
669
+ // ng-options="value.name for value in values"
670
+ // ng-model="selected"
671
+ createSingleSelect();
672
+
673
+ scope.$apply(() => {
674
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
675
+ scope.selected = scope.values[0];
676
+ });
677
+
678
+ const options = element.find("option");
679
+
680
+ expect(scope.selected).toEqual(jasmine.objectContaining({ name: "A" }));
681
+ expect(options.eq(0).prop("selected")).toBe(true);
682
+ expect(options.eq(1).prop("selected")).toBe(false);
683
+
684
+ const optionToSelect = options.eq(1);
685
+
686
+ expect(optionToSelect.text()).toBe("B");
687
+
688
+ optionToSelect.prop("selected", true);
689
+ scope.$digest();
690
+
691
+ expect(optionToSelect.prop("selected")).toBe(true);
692
+ expect(scope.selected).toBe(scope.values[0]);
693
+ });
694
+
695
+ // bug fix #9621
696
+ it("should update the label property", () => {
697
+ // ng-options="value.name for value in values"
698
+ // ng-model="selected"
699
+ createSingleSelect();
700
+
701
+ scope.$apply(() => {
702
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
703
+ scope.selected = scope.values[0];
704
+ });
705
+
706
+ const options = element.find("option");
707
+ expect(options.eq(0).prop("label")).toEqual("A");
708
+ expect(options.eq(1).prop("label")).toEqual("B");
709
+ expect(options.eq(2).prop("label")).toEqual("C");
710
+ });
711
+
712
+ it("should update the label if only the property has changed", () => {
713
+ // ng-options="value.name for value in values"
714
+ // ng-model="selected"
715
+ createSingleSelect();
716
+
717
+ scope.$apply(() => {
718
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
719
+ scope.selected = scope.values[0];
720
+ });
721
+
722
+ let options = element.find("option");
723
+ expect(options.eq(0).prop("label")).toEqual("A");
724
+ expect(options.eq(1).prop("label")).toEqual("B");
725
+ expect(options.eq(2).prop("label")).toEqual("C");
726
+
727
+ scope.$apply('values[0].name = "X"');
728
+
729
+ options = element.find("option");
730
+ expect(options.eq(0).prop("label")).toEqual("X");
731
+ });
732
+
733
+ // bug fix #9714
734
+ it("should select the matching option when the options are updated", () => {
735
+ // first set up a select with no options
736
+ scope.selected = "";
737
+ createSelect({
738
+ "ng-options": "val.id as val.label for val in values",
739
+ "ng-model": "selected",
740
+ });
741
+ let options = element.find("option");
742
+ // we expect the selected option to be the "unknown" option
743
+ expect(options.eq(0)).toEqualUnknownOption("");
744
+ expect(options.eq(0).prop("selected")).toEqual(true);
745
+
746
+ // now add some real options - one of which matches the selected value
747
+ scope.$apply(
748
+ 'values = [{id:"",label:"A"},{id:"1",label:"B"},{id:"2",label:"C"}]',
749
+ );
750
+
751
+ // we expect the selected option to be the one that matches the correct item
752
+ // and for the unknown option to have been removed
753
+ options = element.find("option");
754
+ expect(element).toEqualSelectValue("");
755
+ expect(options.eq(0)).toEqualOption("", "A");
756
+ });
757
+
758
+ it("should be possible to use one-time binding on the expression", () => {
759
+ createSelect({
760
+ "ng-model": "someModel",
761
+ "ng-options": "o as o for o in ::arr",
762
+ });
763
+
764
+ let options;
765
+
766
+ // Initially the options list is just the unknown option
767
+ options = element.find("option");
768
+ expect(options.length).toEqual(1);
769
+
770
+ // Now initialize the scope and the options should be updated
771
+ scope.$apply(() => {
772
+ scope.arr = ["a", "b", "c"];
773
+ });
774
+ options = element.find("option");
775
+ expect(options.length).toEqual(4);
776
+ expect(options.eq(0)).toEqualUnknownOption();
777
+ expect(options.eq(1)).toEqualOption("a");
778
+ expect(options.eq(2)).toEqualOption("b");
779
+ expect(options.eq(3)).toEqualOption("c");
780
+
781
+ // Change the scope but the options should not change
782
+ scope.arr = ["w", "x", "y", "z"];
783
+ scope.$digest();
784
+ options = element.find("option");
785
+ expect(options.length).toEqual(4);
786
+ expect(options.eq(0)).toEqualUnknownOption();
787
+ expect(options.eq(1)).toEqualOption("a");
788
+ expect(options.eq(2)).toEqualOption("b");
789
+ expect(options.eq(3)).toEqualOption("c");
790
+ });
791
+
792
+ it('should remove the "selected" attribute from the previous option when the model changes', () => {
793
+ scope.values = [
794
+ { id: 10, label: "ten" },
795
+ { id: 20, label: "twenty" },
796
+ ];
797
+
798
+ createSelect(
799
+ {
800
+ "ng-model": "selected",
801
+ "ng-options": "item.label for item in values",
802
+ },
803
+ true,
804
+ );
805
+
806
+ let options = element.find("option");
807
+ expect(options[0].selected).toBe(true);
808
+ expect(options[1].selected).not.toBe(true);
809
+ expect(options[2].selected).not.toBe(true);
810
+
811
+ scope.selected = scope.values[0];
812
+ scope.$digest();
813
+
814
+ expect(options[0].selected).not.toBe(true);
815
+ expect(options[1].selected).toBe(true);
816
+ expect(options[2].selected).not.toBe(true);
817
+
818
+ scope.selected = scope.values[1];
819
+ scope.$digest();
820
+
821
+ expect(options[0].selected).not.toBe(true);
822
+ expect(options[1].selected).not.toBe(true);
823
+ expect(options[2].selected).toBe(true);
824
+
825
+ // This will select the empty option
826
+ scope.selected = null;
827
+ scope.$digest();
828
+
829
+ expect(options[0].selected).toBe(true);
830
+ expect(options[1].selected).not.toBe(true);
831
+ expect(options[2].selected).not.toBe(true);
832
+
833
+ // This will add and select the unknown option
834
+ scope.selected = "unmatched value";
835
+ scope.$digest();
836
+ options = element.find("option");
837
+
838
+ expect(options[0].selected).toBe(true);
839
+ expect(options[1].selected).not.toBe(true);
840
+ expect(options[2].selected).not.toBe(true);
841
+ expect(options[3].selected).not.toBe(true);
842
+
843
+ // Back to matched value
844
+ scope.selected = scope.values[1];
845
+ scope.$digest();
846
+ options = element.find("option");
847
+
848
+ expect(options[0].selected).not.toBe(true);
849
+ expect(options[1].selected).not.toBe(true);
850
+ expect(options[2].selected).toBe(true);
851
+ });
852
+
853
+ if (window.MutationObserver) {
854
+ // IE9 and IE10 do not support MutationObserver
855
+ // Since the feature is only needed for a test, it's okay to skip these browsers
856
+ it("should render the initial options only one time", () => {
857
+ scope.value = "black";
858
+ scope.values = ["black", "white", "red"];
859
+ // observe-child-list adds a MutationObserver that we will read out after ngOptions
860
+ // has been compiled
861
+ createSelect({
862
+ "ng-model": "value",
863
+ "ng-options": "value.name for value in values",
864
+ "observe-child-list": "",
865
+ });
866
+
867
+ const optionEls = element[0].querySelectorAll("option");
868
+ const records = childListMutationObserver.takeRecords();
869
+
870
+ expect(records.length).toBe(1);
871
+ expect(records[0].addedNodes).toEqual(optionEls);
872
+ });
873
+ }
874
+
875
+ describe("disableWhen expression", () => {
876
+ describe("on single select", () => {
877
+ it("should disable options", () => {
878
+ scope.selected = "";
879
+ scope.options = [
880
+ { name: "white", value: "#FFFFFF" },
881
+ { name: "one", value: 1, unavailable: true },
882
+ { name: "notTrue", value: false },
883
+ { name: "thirty", value: 30, unavailable: false },
884
+ ];
885
+ createSelect({
886
+ "ng-options":
887
+ "o.value as o.name disable when o.unavailable for o in options",
888
+ "ng-model": "selected",
889
+ });
890
+ const options = element.find("option");
891
+
892
+ expect(options.length).toEqual(5);
893
+ expect(options.eq(1).prop("disabled")).toEqual(false);
894
+ expect(options.eq(2).prop("disabled")).toEqual(true);
895
+ expect(options.eq(3).prop("disabled")).toEqual(false);
896
+ expect(options.eq(4).prop("disabled")).toEqual(false);
897
+ });
898
+
899
+ it("should select disabled options when model changes", () => {
900
+ scope.options = [
901
+ { name: "white", value: "#FFFFFF" },
902
+ { name: "one", value: 1, unavailable: true },
903
+ { name: "notTrue", value: false },
904
+ { name: "thirty", value: 30, unavailable: false },
905
+ ];
906
+ createSelect({
907
+ "ng-options":
908
+ "o.value as o.name disable when o.unavailable for o in options",
909
+ "ng-model": "selected",
910
+ });
911
+
912
+ // Initially the model is set to an enabled option
913
+ scope.$apply("selected = 30");
914
+ let options = element.find("option");
915
+ expect(options.eq(3).prop("selected")).toEqual(true);
916
+
917
+ // Now set the model to a disabled option
918
+ scope.$apply("selected = 1");
919
+ options = element.find("option");
920
+
921
+ // jQuery returns null for val() when the option is disabled, see
922
+ // https://bugs.jquery.com/ticket/13097
923
+ expect(element[0].value).toBe("number:1");
924
+ expect(options.length).toEqual(4);
925
+ expect(options.eq(0).prop("selected")).toEqual(false);
926
+ expect(options.eq(1).prop("selected")).toEqual(true);
927
+ expect(options.eq(2).prop("selected")).toEqual(false);
928
+ expect(options.eq(3).prop("selected")).toEqual(false);
929
+ });
930
+
931
+ it("should select options in model when they become enabled", () => {
932
+ scope.options = [
933
+ { name: "white", value: "#FFFFFF" },
934
+ { name: "one", value: 1, unavailable: true },
935
+ { name: "notTrue", value: false },
936
+ { name: "thirty", value: 30, unavailable: false },
937
+ ];
938
+ createSelect({
939
+ "ng-options":
940
+ "o.value as o.name disable when o.unavailable for o in options",
941
+ "ng-model": "selected",
942
+ });
943
+
944
+ // Set the model to a disabled option
945
+ scope.$apply("selected = 1");
946
+ let options = element.find("option");
947
+
948
+ // jQuery returns null for val() when the option is disabled, see
949
+ // https://bugs.jquery.com/ticket/13097
950
+ expect(element[0].value).toBe("number:1");
951
+ expect(options.length).toEqual(4);
952
+ expect(options.eq(0).prop("selected")).toEqual(false);
953
+ expect(options.eq(1).prop("selected")).toEqual(true);
954
+ expect(options.eq(2).prop("selected")).toEqual(false);
955
+ expect(options.eq(3).prop("selected")).toEqual(false);
956
+
957
+ // Now enable that option
958
+ scope.$apply(() => {
959
+ scope.options[1].unavailable = false;
960
+ });
961
+
962
+ expect(element).toEqualSelectValue(1);
963
+ options = element.find("option");
964
+ expect(options.length).toEqual(4);
965
+ expect(options.eq(1).prop("selected")).toEqual(true);
966
+ expect(options.eq(3).prop("selected")).toEqual(false);
967
+ });
968
+ });
969
+
970
+ describe("on multi select", () => {
971
+ it("should disable options", () => {
972
+ scope.selected = [];
973
+ scope.options = [
974
+ { name: "a", value: 0 },
975
+ { name: "b", value: 1, unavailable: true },
976
+ { name: "c", value: 2 },
977
+ { name: "d", value: 3, unavailable: false },
978
+ ];
979
+ createSelect({
980
+ "ng-options":
981
+ "o.value as o.name disable when o.unavailable for o in options",
982
+ multiple: true,
983
+ "ng-model": "selected",
984
+ });
985
+ const options = element.find("option");
986
+
987
+ expect(options.eq(0).prop("disabled")).toEqual(false);
988
+ expect(options.eq(1).prop("disabled")).toEqual(true);
989
+ expect(options.eq(2).prop("disabled")).toEqual(false);
990
+ expect(options.eq(3).prop("disabled")).toEqual(false);
991
+ });
992
+
993
+ it("should select disabled options when model changes", () => {
994
+ scope.options = [
995
+ { name: "a", value: 0 },
996
+ { name: "b", value: 1, unavailable: true },
997
+ { name: "c", value: 2 },
998
+ { name: "d", value: 3, unavailable: false },
999
+ ];
1000
+ createSelect({
1001
+ "ng-options":
1002
+ "o.value as o.name disable when o.unavailable for o in options",
1003
+ multiple: true,
1004
+ "ng-model": "selected",
1005
+ });
1006
+
1007
+ // Initially the model is set to an enabled option
1008
+ scope.$apply("selected = [3]");
1009
+ let options = element.find("option");
1010
+ expect(options.eq(0).prop("selected")).toEqual(false);
1011
+ expect(options.eq(1).prop("selected")).toEqual(false);
1012
+ expect(options.eq(2).prop("selected")).toEqual(false);
1013
+ expect(options.eq(3).prop("selected")).toEqual(true);
1014
+
1015
+ // Now add a disabled option
1016
+ scope.$apply("selected = [1,3]");
1017
+ options = element.find("option");
1018
+ expect(options.eq(0).prop("selected")).toEqual(false);
1019
+ expect(options.eq(1).prop("selected")).toEqual(true);
1020
+ expect(options.eq(2).prop("selected")).toEqual(false);
1021
+ expect(options.eq(3).prop("selected")).toEqual(true);
1022
+
1023
+ // Now only select the disabled option
1024
+ scope.$apply("selected = [1]");
1025
+ expect(options.eq(0).prop("selected")).toEqual(false);
1026
+ expect(options.eq(1).prop("selected")).toEqual(true);
1027
+ expect(options.eq(2).prop("selected")).toEqual(false);
1028
+ expect(options.eq(3).prop("selected")).toEqual(false);
1029
+ });
1030
+
1031
+ it("should select options in model when they become enabled", () => {
1032
+ scope.options = [
1033
+ { name: "a", value: 0 },
1034
+ { name: "b", value: 1, unavailable: true },
1035
+ { name: "c", value: 2 },
1036
+ { name: "d", value: 3, unavailable: false },
1037
+ ];
1038
+ createSelect({
1039
+ "ng-options":
1040
+ "o.value as o.name disable when o.unavailable for o in options",
1041
+ multiple: true,
1042
+ "ng-model": "selected",
1043
+ });
1044
+
1045
+ // Set the model to a disabled option
1046
+ scope.$apply("selected = [1]");
1047
+ let options = element.find("option");
1048
+
1049
+ expect(options.eq(0).prop("selected")).toEqual(false);
1050
+ expect(options.eq(1).prop("selected")).toEqual(true);
1051
+ expect(options.eq(2).prop("selected")).toEqual(false);
1052
+ expect(options.eq(3).prop("selected")).toEqual(false);
1053
+
1054
+ // Now enable that option
1055
+ scope.$apply(() => {
1056
+ scope.options[1].unavailable = false;
1057
+ });
1058
+
1059
+ expect(element).toEqualSelectValue([1], true);
1060
+ options = element.find("option");
1061
+ expect(options.eq(0).prop("selected")).toEqual(false);
1062
+ expect(options.eq(1).prop("selected")).toEqual(true);
1063
+ expect(options.eq(2).prop("selected")).toEqual(false);
1064
+ expect(options.eq(3).prop("selected")).toEqual(false);
1065
+ });
1066
+ });
1067
+ });
1068
+
1069
+ describe("selectAs expression", () => {
1070
+ beforeEach(() => {
1071
+ scope.arr = [
1072
+ { id: 10, label: "ten" },
1073
+ { id: 20, label: "twenty" },
1074
+ ];
1075
+ scope.obj = {
1076
+ 10: { score: 10, label: "ten" },
1077
+ 20: { score: 20, label: "twenty" },
1078
+ };
1079
+ });
1080
+
1081
+ it("should support single select with array source", () => {
1082
+ createSelect({
1083
+ "ng-model": "selected",
1084
+ "ng-options": "item.id as item.label for item in arr",
1085
+ });
1086
+
1087
+ scope.$apply(() => {
1088
+ scope.selected = 10;
1089
+ });
1090
+ expect(element).toEqualSelectValue(10);
1091
+
1092
+ setSelectValue(element, 1);
1093
+ expect(scope.selected).toBe(20);
1094
+ });
1095
+
1096
+ it("should support multi select with array source", () => {
1097
+ createSelect({
1098
+ "ng-model": "selected",
1099
+ multiple: true,
1100
+ "ng-options": "item.id as item.label for item in arr",
1101
+ });
1102
+
1103
+ scope.$apply(() => {
1104
+ scope.selected = [10, 20];
1105
+ });
1106
+ expect(element).toEqualSelectValue([10, 20], true);
1107
+ expect(scope.selected).toEqual([10, 20]);
1108
+
1109
+ element.children()[0].selected = false;
1110
+ browserTrigger(element, "change");
1111
+ expect(scope.selected).toEqual([20]);
1112
+ expect(element).toEqualSelectValue([20], true);
1113
+ });
1114
+
1115
+ it("should re-render if an item in an array source is added/removed", () => {
1116
+ createSelect({
1117
+ "ng-model": "selected",
1118
+ multiple: true,
1119
+ "ng-options": "item.id as item.label for item in arr",
1120
+ });
1121
+
1122
+ scope.$apply(() => {
1123
+ scope.selected = [10];
1124
+ });
1125
+ expect(element).toEqualSelectValue([10], true);
1126
+
1127
+ scope.$apply(() => {
1128
+ scope.selected.push(20);
1129
+ });
1130
+ expect(element).toEqualSelectValue([10, 20], true);
1131
+
1132
+ scope.$apply(() => {
1133
+ scope.selected.shift();
1134
+ });
1135
+ expect(element).toEqualSelectValue([20], true);
1136
+ });
1137
+
1138
+ it("should handle a options containing circular references", () => {
1139
+ scope.arr[0].ref = scope.arr[0];
1140
+ scope.selected = [scope.arr[0]];
1141
+ createSelect({
1142
+ "ng-model": "selected",
1143
+ multiple: true,
1144
+ "ng-options": "item as item.label for item in arr",
1145
+ });
1146
+ expect(element).toEqualSelectValue([scope.arr[0]], true);
1147
+
1148
+ scope.$apply(() => {
1149
+ scope.selected.push(scope.arr[1]);
1150
+ });
1151
+ expect(element).toEqualSelectValue([scope.arr[0], scope.arr[1]], true);
1152
+
1153
+ scope.$apply(() => {
1154
+ scope.selected.pop();
1155
+ });
1156
+ expect(element).toEqualSelectValue([scope.arr[0]], true);
1157
+ });
1158
+
1159
+ it("should support single select with object source", () => {
1160
+ createSelect({
1161
+ "ng-model": "selected",
1162
+ "ng-options": "val.score as val.label for (key, val) in obj",
1163
+ });
1164
+
1165
+ scope.$apply(() => {
1166
+ scope.selected = 10;
1167
+ });
1168
+ expect(element).toEqualSelectValue(10);
1169
+
1170
+ setSelectValue(element, 1);
1171
+ expect(scope.selected).toBe(20);
1172
+ });
1173
+
1174
+ it("should support multi select with object source", () => {
1175
+ createSelect({
1176
+ "ng-model": "selected",
1177
+ multiple: true,
1178
+ "ng-options": "val.score as val.label for (key, val) in obj",
1179
+ });
1180
+
1181
+ scope.$apply(() => {
1182
+ scope.selected = [10, 20];
1183
+ });
1184
+ expect(element).toEqualSelectValue([10, 20], true);
1185
+
1186
+ element.children()[0].selected = false;
1187
+ browserTrigger(element, "change");
1188
+ expect(scope.selected).toEqual([20]);
1189
+ expect(element).toEqualSelectValue([20], true);
1190
+ });
1191
+ });
1192
+
1193
+ describe("trackBy expression", () => {
1194
+ beforeEach(() => {
1195
+ scope.arr = [
1196
+ { id: 10, label: "ten" },
1197
+ { id: 20, label: "twenty" },
1198
+ ];
1199
+ scope.obj = {
1200
+ 1: { score: 10, label: "ten" },
1201
+ 2: { score: 20, label: "twenty" },
1202
+ };
1203
+ });
1204
+
1205
+ it("should set the result of track by expression to element value", () => {
1206
+ createSelect({
1207
+ "ng-model": "selected",
1208
+ "ng-options": "item.label for item in arr track by item.id",
1209
+ });
1210
+
1211
+ expect(element.val()).toEqualUnknownValue();
1212
+
1213
+ scope.$apply(() => {
1214
+ scope.selected = scope.arr[0];
1215
+ });
1216
+ expect(element.val()).toBe("10");
1217
+
1218
+ scope.$apply(() => {
1219
+ scope.arr[0] = { id: 10, label: "new ten" };
1220
+ });
1221
+ expect(element.val()).toBe("10");
1222
+
1223
+ element.children()[1].selected = "selected";
1224
+ browserTrigger(element, "change");
1225
+ expect(scope.selected).toEqual(scope.arr[1]);
1226
+ });
1227
+
1228
+ it("should use the tracked expression as option value", () => {
1229
+ createSelect({
1230
+ "ng-model": "selected",
1231
+ "ng-options": "item.label for item in arr track by item.id",
1232
+ });
1233
+
1234
+ const options = element.find("option");
1235
+ expect(options.length).toEqual(3);
1236
+ expect(options.eq(0)).toEqualUnknownOption();
1237
+ expect(options.eq(1)).toEqualTrackedOption(10, "ten");
1238
+ expect(options.eq(2)).toEqualTrackedOption(20, "twenty");
1239
+ });
1240
+
1241
+ it("should update the selected option even if only the tracked property on the selected object changes (single)", () => {
1242
+ createSelect({
1243
+ "ng-model": "selected",
1244
+ "ng-options": "item.label for item in arr track by item.id",
1245
+ });
1246
+
1247
+ scope.$apply(() => {
1248
+ scope.selected = { id: 10, label: "ten" };
1249
+ });
1250
+
1251
+ expect(element.val()).toEqual("10");
1252
+
1253
+ // Update the properties on the selected object, rather than replacing the whole object
1254
+ scope.$apply(() => {
1255
+ scope.selected.id = 20;
1256
+ scope.selected.label = "new twenty";
1257
+ });
1258
+
1259
+ // The value of the select should change since the id property changed
1260
+ expect(element.val()).toEqual("20");
1261
+
1262
+ // But the label of the selected option does not change
1263
+ const option = element.find("option").eq(1);
1264
+ expect(option.prop("selected")).toEqual(true);
1265
+ expect(option.text()).toEqual("twenty"); // not 'new twenty'
1266
+ });
1267
+
1268
+ it(
1269
+ "should update the selected options even if only the tracked properties on the objects in the " +
1270
+ "selected collection change (multi)",
1271
+ () => {
1272
+ createSelect({
1273
+ "ng-model": "selected",
1274
+ multiple: true,
1275
+ "ng-options": "item.label for item in arr track by item.id",
1276
+ });
1277
+
1278
+ scope.$apply(() => {
1279
+ scope.selected = [{ id: 10, label: "ten" }];
1280
+ });
1281
+
1282
+ expect(element.val()).toEqual(["10"]);
1283
+
1284
+ // Update the tracked property on the object in the selected array, rather than replacing the whole object
1285
+ scope.$apply(() => {
1286
+ scope.selected[0].id = 20;
1287
+ });
1288
+
1289
+ // The value of the select should change since the id property changed
1290
+ expect(element.val()).toEqual(["20"]);
1291
+
1292
+ // But the label of the selected option does not change
1293
+ const option = element.find("option").eq(1);
1294
+ expect(option.prop("selected")).toEqual(true);
1295
+ expect(option.text()).toEqual("twenty"); // not 'new twenty'
1296
+ },
1297
+ );
1298
+
1299
+ it("should prevent changes to the selected object from modifying the options objects (single)", () => {
1300
+ createSelect({
1301
+ "ng-model": "selected",
1302
+ "ng-options": "item.label for item in arr track by item.id",
1303
+ });
1304
+
1305
+ element.val("10");
1306
+ browserTrigger(element, "change");
1307
+
1308
+ expect(scope.selected).toEqual(scope.arr[0]);
1309
+
1310
+ scope.$apply(() => {
1311
+ scope.selected.id = 20;
1312
+ });
1313
+
1314
+ expect(scope.selected).not.toEqual(scope.arr[0]);
1315
+ expect(element.val()).toEqual("20");
1316
+ expect(scope.arr).toEqual([
1317
+ { id: 10, label: "ten" },
1318
+ { id: 20, label: "twenty" },
1319
+ ]);
1320
+ });
1321
+
1322
+ it("should preserve value even when reference has changed (single&array)", () => {
1323
+ createSelect({
1324
+ "ng-model": "selected",
1325
+ "ng-options": "item.label for item in arr track by item.id",
1326
+ });
1327
+
1328
+ scope.$apply(() => {
1329
+ scope.selected = scope.arr[0];
1330
+ });
1331
+ expect(element.val()).toBe("10");
1332
+
1333
+ scope.$apply(() => {
1334
+ scope.arr[0] = { id: 10, label: "new ten" };
1335
+ });
1336
+ expect(element.val()).toBe("10");
1337
+
1338
+ element.children()[1].selected = 1;
1339
+ browserTrigger(element, "change");
1340
+ expect(scope.selected).toEqual(scope.arr[1]);
1341
+ });
1342
+
1343
+ it("should preserve value even when reference has changed (multi&array)", () => {
1344
+ createSelect({
1345
+ "ng-model": "selected",
1346
+ multiple: true,
1347
+ "ng-options": "item.label for item in arr track by item.id",
1348
+ });
1349
+
1350
+ scope.$apply(() => {
1351
+ scope.selected = scope.arr;
1352
+ });
1353
+ expect(element.val()).toEqual(["10", "20"]);
1354
+
1355
+ scope.$apply(() => {
1356
+ scope.arr[0] = { id: 10, label: "new ten" };
1357
+ });
1358
+ expect(element.val()).toEqual(["10", "20"]);
1359
+
1360
+ element.children()[0].selected = false;
1361
+ browserTrigger(element, "change");
1362
+ expect(scope.selected).toEqual([scope.arr[1]]);
1363
+ });
1364
+
1365
+ it("should preserve value even when reference has changed (single&object)", () => {
1366
+ createSelect({
1367
+ "ng-model": "selected",
1368
+ "ng-options": "val.label for (key, val) in obj track by val.score",
1369
+ });
1370
+
1371
+ scope.$apply(() => {
1372
+ scope.selected = scope.obj["1"];
1373
+ });
1374
+ expect(element.val()).toBe("10");
1375
+
1376
+ scope.$apply(() => {
1377
+ scope.obj["1"] = { score: 10, label: "ten" };
1378
+ });
1379
+ expect(element.val()).toBe("10");
1380
+
1381
+ setSelectValue(element, 1);
1382
+ expect(scope.selected).toEqual(scope.obj["2"]);
1383
+ });
1384
+
1385
+ it("should preserve value even when reference has changed (multi&object)", () => {
1386
+ createSelect({
1387
+ "ng-model": "selected",
1388
+ multiple: true,
1389
+ "ng-options": "val.label for (key, val) in obj track by val.score",
1390
+ });
1391
+
1392
+ scope.$apply(() => {
1393
+ scope.selected = [scope.obj["1"]];
1394
+ });
1395
+ expect(element.val()).toEqual(["10"]);
1396
+
1397
+ scope.$apply(() => {
1398
+ scope.obj["1"] = { score: 10, label: "ten" };
1399
+ });
1400
+ expect(element.val()).toEqual(["10"]);
1401
+
1402
+ element.children()[1].selected = "selected";
1403
+ browserTrigger(element, "change");
1404
+ expect(scope.selected).toEqual([scope.obj["1"], scope.obj["2"]]);
1405
+ });
1406
+
1407
+ it("should prevent infinite digest if track by expression is stable", () => {
1408
+ scope.makeOptions = function () {
1409
+ const options = [];
1410
+ for (let i = 0; i < 5; i++) {
1411
+ options.push({ label: `Value = ${i}`, value: i });
1412
+ }
1413
+ return options;
1414
+ };
1415
+ scope.selected = { label: "Value = 1", value: 1 };
1416
+ expect(() => {
1417
+ createSelect({
1418
+ "ng-model": "selected",
1419
+ "ng-options":
1420
+ "item.label for item in makeOptions() track by item.value",
1421
+ });
1422
+ }).not.toThrow();
1423
+ });
1424
+
1425
+ it("should re-render if the tracked property of the model is changed when using trackBy", () => {
1426
+ createSelect({
1427
+ "ng-model": "selected",
1428
+ "ng-options": "item for item in arr track by item.id",
1429
+ });
1430
+
1431
+ scope.$apply(() => {
1432
+ scope.selected = { id: 10, label: "ten" };
1433
+ });
1434
+
1435
+ spyOn(element.controller("ngModel"), "$render");
1436
+
1437
+ scope.$apply(() => {
1438
+ scope.arr[0].id = 20;
1439
+ });
1440
+
1441
+ // update render due to equality watch
1442
+ expect(element.controller("ngModel").$render).toHaveBeenCalled();
1443
+ });
1444
+
1445
+ it("should not set view value again if the tracked property of the model has not changed when using trackBy", () => {
1446
+ createSelect({
1447
+ "ng-model": "selected",
1448
+ "ng-options": "item for item in arr track by item.id",
1449
+ });
1450
+
1451
+ scope.$apply(() => {
1452
+ scope.selected = { id: 10, label: "ten" };
1453
+ });
1454
+
1455
+ spyOn(element.controller("ngModel"), "$setViewValue");
1456
+
1457
+ scope.$apply(() => {
1458
+ scope.arr[0] = { id: 10, label: "ten" };
1459
+ });
1460
+
1461
+ expect(
1462
+ element.controller("ngModel").$setViewValue,
1463
+ ).not.toHaveBeenCalled();
1464
+ });
1465
+
1466
+ it("should not re-render if a property of the model is changed when not using trackBy", () => {
1467
+ createSelect({
1468
+ "ng-model": "selected",
1469
+ "ng-options": "item for item in arr",
1470
+ });
1471
+
1472
+ scope.$apply(() => {
1473
+ scope.selected = scope.arr[0];
1474
+ });
1475
+
1476
+ spyOn(element.controller("ngModel"), "$render");
1477
+
1478
+ scope.$apply(() => {
1479
+ scope.selected.label = "changed";
1480
+ });
1481
+
1482
+ // no render update as no equality watch
1483
+ expect(element.controller("ngModel").$render).not.toHaveBeenCalled();
1484
+ });
1485
+
1486
+ it("should handle options containing circular references (single)", () => {
1487
+ scope.arr[0].ref = scope.arr[0];
1488
+ createSelect({
1489
+ "ng-model": "selected",
1490
+ "ng-options": "item for item in arr track by item.id",
1491
+ });
1492
+
1493
+ expect(() => {
1494
+ scope.$apply(() => {
1495
+ scope.selected = scope.arr[0];
1496
+ });
1497
+ }).not.toThrow();
1498
+ });
1499
+
1500
+ it("should handle options containing circular references (multiple)", () => {
1501
+ scope.arr[0].ref = scope.arr[0];
1502
+ createSelect({
1503
+ "ng-model": "selected",
1504
+ multiple: true,
1505
+ "ng-options": "item for item in arr track by item.id",
1506
+ });
1507
+
1508
+ expect(() => {
1509
+ scope.$apply(() => {
1510
+ scope.selected = [scope.arr[0]];
1511
+ });
1512
+
1513
+ scope.$apply(() => {
1514
+ scope.selected.push(scope.arr[1]);
1515
+ });
1516
+ }).not.toThrow();
1517
+ });
1518
+
1519
+ it('should remove the "selected" attribute when the model changes', () => {
1520
+ createSelect({
1521
+ "ng-model": "selected",
1522
+ "ng-options": "item.label for item in arr track by item.id",
1523
+ });
1524
+
1525
+ const options = element.find("option");
1526
+ element[0].selectedIndex = 2;
1527
+ element[0].dispatchEvent(new Event("change"));
1528
+ expect(scope.selected).toEqual(scope.arr[1]);
1529
+
1530
+ scope.selected = {};
1531
+ scope.$digest();
1532
+ expect(options[0].selected).toBeTrue();
1533
+ expect(options[1].selected).not.toBeTrue();
1534
+ expect(options[2].selected).not.toBeTrue();
1535
+ });
1536
+ });
1537
+
1538
+ /**
1539
+ * This behavior is broken and should probably be cleaned up later as track by and select as
1540
+ * aren't compatible.
1541
+ */
1542
+ describe("selectAs+trackBy expression", () => {
1543
+ beforeEach(() => {
1544
+ scope.arr = [
1545
+ { subItem: { label: "ten", id: 10 } },
1546
+ { subItem: { label: "twenty", id: 20 } },
1547
+ ];
1548
+ scope.obj = {
1549
+ 10: { subItem: { id: 10, label: "ten" } },
1550
+ 20: { subItem: { id: 20, label: "twenty" } },
1551
+ };
1552
+ });
1553
+
1554
+ it(
1555
+ 'It should use the "value" variable to represent items in the array as well as for the ' +
1556
+ "selected values in track by expression (single&array)",
1557
+ () => {
1558
+ createSelect({
1559
+ "ng-model": "selected",
1560
+ "ng-options":
1561
+ "item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)",
1562
+ });
1563
+
1564
+ // First test model -> view
1565
+
1566
+ scope.$apply(() => {
1567
+ scope.selected = scope.arr[0].subItem;
1568
+ });
1569
+ expect(element.val()).toEqual("10");
1570
+
1571
+ scope.$apply(() => {
1572
+ scope.selected = scope.arr[1].subItem;
1573
+ });
1574
+ expect(element.val()).toEqual("20");
1575
+
1576
+ // Now test view -> model
1577
+
1578
+ element.val("10");
1579
+ browserTrigger(element, "change");
1580
+ expect(scope.selected).toEqual(scope.arr[0].subItem);
1581
+
1582
+ // Now reload the array
1583
+ scope.$apply(() => {
1584
+ scope.arr = [
1585
+ {
1586
+ subItem: { label: "new ten", id: 10 },
1587
+ },
1588
+ {
1589
+ subItem: { label: "new twenty", id: 20 },
1590
+ },
1591
+ ];
1592
+ });
1593
+ expect(element.val()).toBe("10");
1594
+ expect(scope.selected.id).toBe(10);
1595
+ },
1596
+ );
1597
+
1598
+ it(
1599
+ 'It should use the "value" variable to represent items in the array as well as for the ' +
1600
+ "selected values in track by expression (multiple&array)",
1601
+ () => {
1602
+ createSelect({
1603
+ "ng-model": "selected",
1604
+ multiple: true,
1605
+ "ng-options":
1606
+ "item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)",
1607
+ });
1608
+
1609
+ // First test model -> view
1610
+
1611
+ scope.$apply(() => {
1612
+ scope.selected = [scope.arr[0].subItem];
1613
+ });
1614
+ expect(element.val()).toEqual(["10"]);
1615
+
1616
+ scope.$apply(() => {
1617
+ scope.selected = [scope.arr[1].subItem];
1618
+ });
1619
+ expect(element.val()).toEqual(["20"]);
1620
+
1621
+ // Now test view -> model
1622
+
1623
+ element.find("option")[0].selected = true;
1624
+ element.find("option")[1].selected = false;
1625
+ browserTrigger(element, "change");
1626
+ expect(scope.selected).toEqual([scope.arr[0].subItem]);
1627
+
1628
+ // Now reload the array
1629
+ scope.$apply(() => {
1630
+ scope.arr = [
1631
+ {
1632
+ subItem: { label: "new ten", id: 10 },
1633
+ },
1634
+ {
1635
+ subItem: { label: "new twenty", id: 20 },
1636
+ },
1637
+ ];
1638
+ });
1639
+ expect(element.val()).toEqual(["10"]);
1640
+ expect(scope.selected[0].id).toEqual(10);
1641
+ expect(scope.selected.length).toBe(1);
1642
+ },
1643
+ );
1644
+
1645
+ it(
1646
+ 'It should use the "value" variable to represent items in the array as well as for the ' +
1647
+ "selected values in track by expression (multiple&object)",
1648
+ () => {
1649
+ createSelect({
1650
+ "ng-model": "selected",
1651
+ multiple: true,
1652
+ "ng-options":
1653
+ "val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)",
1654
+ });
1655
+
1656
+ // First test model -> view
1657
+
1658
+ scope.$apply(() => {
1659
+ scope.selected = [scope.obj["10"].subItem];
1660
+ });
1661
+ expect(element.val()).toEqual(["10"]);
1662
+
1663
+ scope.$apply(() => {
1664
+ scope.selected = [scope.obj["10"].subItem];
1665
+ });
1666
+ expect(element.val()).toEqual(["10"]);
1667
+
1668
+ // Now test view -> model
1669
+
1670
+ element.find("option")[0].selected = true;
1671
+ element.find("option")[1].selected = false;
1672
+ browserTrigger(element, "change");
1673
+ expect(scope.selected).toEqual([scope.obj["10"].subItem]);
1674
+
1675
+ // Now reload the object
1676
+ scope.$apply(() => {
1677
+ scope.obj = {
1678
+ 10: {
1679
+ subItem: { label: "new ten", id: 10 },
1680
+ },
1681
+ 20: {
1682
+ subItem: { label: "new twenty", id: 20 },
1683
+ },
1684
+ };
1685
+ });
1686
+ expect(element.val()).toEqual(["10"]);
1687
+ expect(scope.selected[0].id).toBe(10);
1688
+ expect(scope.selected.length).toBe(1);
1689
+ },
1690
+ );
1691
+
1692
+ it(
1693
+ 'It should use the "value" variable to represent items in the array as well as for the ' +
1694
+ "selected values in track by expression (single&object)",
1695
+ () => {
1696
+ createSelect({
1697
+ "ng-model": "selected",
1698
+ "ng-options":
1699
+ "val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)",
1700
+ });
1701
+
1702
+ // First test model -> view
1703
+
1704
+ scope.$apply(() => {
1705
+ scope.selected = scope.obj["10"].subItem;
1706
+ });
1707
+ expect(element.val()).toEqual("10");
1708
+
1709
+ scope.$apply(() => {
1710
+ scope.selected = scope.obj["10"].subItem;
1711
+ });
1712
+ expect(element.val()).toEqual("10");
1713
+
1714
+ // Now test view -> model
1715
+
1716
+ element.find("option")[0].selected = true;
1717
+ browserTrigger(element, "change");
1718
+ expect(scope.selected).toEqual(scope.obj["10"].subItem);
1719
+
1720
+ // Now reload the object
1721
+ scope.$apply(() => {
1722
+ scope.obj = {
1723
+ 10: {
1724
+ subItem: { label: "new ten", id: 10 },
1725
+ },
1726
+ 20: {
1727
+ subItem: { label: "new twenty", id: 20 },
1728
+ },
1729
+ };
1730
+ });
1731
+ expect(element.val()).toEqual("10");
1732
+ expect(scope.selected.id).toBe(10);
1733
+ },
1734
+ );
1735
+ });
1736
+
1737
+ describe("binding", () => {
1738
+ it("should bind to scope value", () => {
1739
+ createSingleSelect();
1740
+
1741
+ scope.$apply(() => {
1742
+ scope.values = [{ name: "A" }, { name: "B" }];
1743
+ scope.selected = scope.values[0];
1744
+ });
1745
+
1746
+ expect(element).toEqualSelectValue(scope.selected);
1747
+
1748
+ scope.$apply(() => {
1749
+ scope.selected = scope.values[1];
1750
+ });
1751
+
1752
+ expect(element).toEqualSelectValue(scope.selected);
1753
+ });
1754
+
1755
+ it("should bind to scope value and group", () => {
1756
+ createSelect({
1757
+ "ng-model": "selected",
1758
+ "ng-options": "item.name group by item.group for item in values",
1759
+ });
1760
+
1761
+ scope.$apply(() => {
1762
+ scope.values = [
1763
+ { name: "A" },
1764
+ { name: "B", group: 0 },
1765
+ { name: "C", group: "first" },
1766
+ { name: "D", group: "second" },
1767
+ { name: "E", group: 0 },
1768
+ { name: "F", group: "first" },
1769
+ { name: "G", group: "second" },
1770
+ ];
1771
+ scope.selected = scope.values[3];
1772
+ });
1773
+
1774
+ expect(element).toEqualSelectValue(scope.selected);
1775
+
1776
+ const optgroups = element.find("optgroup");
1777
+ expect(optgroups.length).toBe(3);
1778
+
1779
+ const zero = optgroups.eq(0);
1780
+ const b = zero.find("option").eq(0);
1781
+ const e = zero.find("option").eq(1);
1782
+ expect(zero.attr("label")).toEqual("0");
1783
+ expect(b.text()).toEqual("B");
1784
+ expect(e.text()).toEqual("E");
1785
+
1786
+ const first = optgroups.eq(1);
1787
+ const c = first.find("option").eq(0);
1788
+ const f = first.find("option").eq(1);
1789
+ expect(first.attr("label")).toEqual("first");
1790
+ expect(c.text()).toEqual("C");
1791
+ expect(f.text()).toEqual("F");
1792
+
1793
+ const second = optgroups.eq(2);
1794
+ const d = second.find("option").eq(0);
1795
+ const g = second.find("option").eq(1);
1796
+ expect(second.attr("label")).toEqual("second");
1797
+ expect(d.text()).toEqual("D");
1798
+ expect(g.text()).toEqual("G");
1799
+
1800
+ scope.$apply(() => {
1801
+ scope.selected = scope.values[0];
1802
+ });
1803
+
1804
+ expect(element).toEqualSelectValue(scope.selected);
1805
+ });
1806
+
1807
+ it("should group when the options are available on compile time", () => {
1808
+ scope.values = [
1809
+ { name: "C", group: "first" },
1810
+ { name: "D", group: "second" },
1811
+ { name: "F", group: "first" },
1812
+ { name: "G", group: "second" },
1813
+ ];
1814
+ scope.selected = scope.values[3];
1815
+
1816
+ createSelect({
1817
+ "ng-model": "selected",
1818
+ "ng-options":
1819
+ "item as item.name group by item.group for item in values",
1820
+ });
1821
+
1822
+ expect(element).toEqualSelectValue(scope.selected);
1823
+
1824
+ const optgroups = element.find("optgroup");
1825
+ expect(optgroups.length).toBe(2);
1826
+
1827
+ const first = optgroups.eq(0);
1828
+ const c = first.find("option").eq(0);
1829
+ const f = first.find("option").eq(1);
1830
+ expect(first.attr("label")).toEqual("first");
1831
+ expect(c.text()).toEqual("C");
1832
+ expect(f.text()).toEqual("F");
1833
+
1834
+ const second = optgroups.eq(1);
1835
+ const d = second.find("option").eq(0);
1836
+ const g = second.find("option").eq(1);
1837
+ expect(second.attr("label")).toEqual("second");
1838
+ expect(d.text()).toEqual("D");
1839
+ expect(g.text()).toEqual("G");
1840
+
1841
+ scope.$apply(() => {
1842
+ scope.selected = scope.values[0];
1843
+ });
1844
+
1845
+ expect(element).toEqualSelectValue(scope.selected);
1846
+ });
1847
+
1848
+ it("should group when the options are updated", () => {
1849
+ let optgroups;
1850
+ let one;
1851
+ let two;
1852
+ let three;
1853
+ let alpha;
1854
+ let beta;
1855
+ let gamma;
1856
+ let delta;
1857
+ let epsilon;
1858
+
1859
+ createSelect({
1860
+ "ng-model": "selected",
1861
+ "ng-options": "i.name group by i.cls for i in list",
1862
+ });
1863
+
1864
+ scope.list = [
1865
+ { cls: "one", name: "Alpha" },
1866
+ { cls: "one", name: "Beta" },
1867
+ { cls: "two", name: "Gamma" },
1868
+ ];
1869
+ scope.$digest();
1870
+
1871
+ optgroups = element.find("optgroup");
1872
+ expect(optgroups.length).toBe(2);
1873
+
1874
+ one = optgroups.eq(0);
1875
+ expect(one.children("option").length).toBe(2);
1876
+
1877
+ alpha = one.find("option").eq(0);
1878
+ beta = one.find("option").eq(1);
1879
+ expect(one.attr("label")).toEqual("one");
1880
+ expect(alpha.text()).toEqual("Alpha");
1881
+ expect(beta.text()).toEqual("Beta");
1882
+
1883
+ two = optgroups.eq(1);
1884
+ expect(two.children("option").length).toBe(1);
1885
+
1886
+ gamma = two.find("option").eq(0);
1887
+ expect(two.attr("label")).toEqual("two");
1888
+ expect(gamma.text()).toEqual("Gamma");
1889
+
1890
+ // Remove item from first group, add item to second group, add new group
1891
+ scope.list.shift();
1892
+ scope.list.push(
1893
+ { cls: "two", name: "Delta" },
1894
+ { cls: "three", name: "Epsilon" },
1895
+ );
1896
+ scope.$digest();
1897
+
1898
+ optgroups = element.find("optgroup");
1899
+ expect(optgroups.length).toBe(3);
1900
+
1901
+ // Group with removed item
1902
+ one = optgroups.eq(0);
1903
+ expect(one.children("option").length).toBe(1);
1904
+
1905
+ beta = one.find("option").eq(0);
1906
+ expect(one.attr("label")).toEqual("one");
1907
+ expect(beta.text()).toEqual("Beta");
1908
+
1909
+ // Group with new item
1910
+ two = optgroups.eq(1);
1911
+ expect(two.children("option").length).toBe(2);
1912
+
1913
+ gamma = two.find("option").eq(0);
1914
+ expect(two.attr("label")).toEqual("two");
1915
+ expect(gamma.text()).toEqual("Gamma");
1916
+ delta = two.find("option").eq(1);
1917
+ expect(two.attr("label")).toEqual("two");
1918
+ expect(delta.text()).toEqual("Delta");
1919
+
1920
+ // New group
1921
+ three = optgroups.eq(2);
1922
+ expect(three.children("option").length).toBe(1);
1923
+
1924
+ epsilon = three.find("option").eq(0);
1925
+ expect(three.attr("label")).toEqual("three");
1926
+ expect(epsilon.text()).toEqual("Epsilon");
1927
+ });
1928
+
1929
+ it("should place non-grouped items in the list where they appear", () => {
1930
+ createSelect({
1931
+ "ng-model": "selected",
1932
+ "ng-options": "item.name group by item.group for item in values",
1933
+ });
1934
+
1935
+ scope.$apply(() => {
1936
+ scope.values = [
1937
+ { name: "A" },
1938
+ { name: "B", group: "first" },
1939
+ { name: "C", group: "second" },
1940
+ { name: "D" },
1941
+ { name: "E", group: "first" },
1942
+ { name: "F" },
1943
+ { name: "G" },
1944
+ { name: "H", group: "second" },
1945
+ ];
1946
+ scope.selected = scope.values[0];
1947
+ });
1948
+
1949
+ const children = element.children();
1950
+ expect(children.length).toEqual(6);
1951
+
1952
+ expect(children[0].nodeName.toLowerCase()).toEqual("option");
1953
+ expect(children[1].nodeName.toLowerCase()).toEqual("optgroup");
1954
+ expect(children[2].nodeName.toLowerCase()).toEqual("optgroup");
1955
+ expect(children[3].nodeName.toLowerCase()).toEqual("option");
1956
+ expect(children[4].nodeName.toLowerCase()).toEqual("option");
1957
+ expect(children[5].nodeName.toLowerCase()).toEqual("option");
1958
+ });
1959
+
1960
+ it("should group if the group has a falsy value (except undefined)", () => {
1961
+ createSelect({
1962
+ "ng-model": "selected",
1963
+ "ng-options": "item.name group by item.group for item in values",
1964
+ });
1965
+
1966
+ scope.$apply(() => {
1967
+ scope.values = [
1968
+ { name: "A" },
1969
+ { name: "B", group: "" },
1970
+ { name: "C", group: null },
1971
+ { name: "D", group: false },
1972
+ { name: "E", group: 0 },
1973
+ ];
1974
+ scope.selected = scope.values[0];
1975
+ });
1976
+
1977
+ const optgroups = element.find("optgroup");
1978
+ const options = element.find("option");
1979
+
1980
+ expect(optgroups.length).toEqual(4);
1981
+ expect(options.length).toEqual(5);
1982
+
1983
+ expect(optgroups[0].label).toBe("");
1984
+ expect(optgroups[1].label).toBe("null");
1985
+ expect(optgroups[2].label).toBe("false");
1986
+ expect(optgroups[3].label).toBe("0");
1987
+
1988
+ expect(options[0].textContent).toBe("A");
1989
+ expect(options[0].parentNode).toBe(element[0]);
1990
+
1991
+ expect(options[1].textContent).toBe("B");
1992
+ expect(options[1].parentNode).toBe(optgroups[0]);
1993
+
1994
+ expect(options[2].textContent).toBe("C");
1995
+ expect(options[2].parentNode).toBe(optgroups[1]);
1996
+
1997
+ expect(options[3].textContent).toBe("D");
1998
+ expect(options[3].parentNode).toBe(optgroups[2]);
1999
+
2000
+ expect(options[4].textContent).toBe("E");
2001
+ expect(options[4].parentNode).toBe(optgroups[3]);
2002
+ });
2003
+
2004
+ it("should not duplicate a group with a falsy value when the options are updated", () => {
2005
+ scope.$apply(() => {
2006
+ scope.values = [
2007
+ { value: "A", group: "" },
2008
+ { value: "B", group: "First" },
2009
+ ];
2010
+ scope.selected = scope.values[0];
2011
+ });
2012
+
2013
+ createSelect({
2014
+ "ng-model": "selected",
2015
+ "ng-options": "item.value group by item.group for item in values",
2016
+ });
2017
+
2018
+ scope.$apply(() => {
2019
+ scope.values.push({ value: "C", group: false });
2020
+ });
2021
+
2022
+ const optgroups = element.find("optgroup");
2023
+ const options = element.find("option");
2024
+
2025
+ expect(optgroups.length).toEqual(3);
2026
+ expect(options.length).toEqual(3);
2027
+
2028
+ expect(optgroups[0].label).toBe("");
2029
+ expect(optgroups[1].label).toBe("First");
2030
+ expect(optgroups[2].label).toBe("false");
2031
+
2032
+ expect(options[0].textContent).toBe("A");
2033
+ expect(options[0].parentNode).toBe(optgroups[0]);
2034
+
2035
+ expect(options[1].textContent).toBe("B");
2036
+ expect(options[1].parentNode).toBe(optgroups[1]);
2037
+
2038
+ expect(options[2].textContent).toBe("C");
2039
+ expect(options[2].parentNode).toBe(optgroups[2]);
2040
+ });
2041
+
2042
+ it("should bind to scope value and track/identify objects", () => {
2043
+ createSelect({
2044
+ "ng-model": "selected",
2045
+ "ng-options": "item.name for item in values track by item.id",
2046
+ });
2047
+
2048
+ scope.$apply(() => {
2049
+ scope.values = [
2050
+ { id: 1, name: "first" },
2051
+ { id: 2, name: "second" },
2052
+ { id: 3, name: "third" },
2053
+ { id: 4, name: "forth" },
2054
+ ];
2055
+ scope.selected = scope.values[1];
2056
+ });
2057
+
2058
+ expect(element.val()).toEqual("2");
2059
+
2060
+ const first = jqLite(element.find("option")[0]);
2061
+ expect(first.text()).toEqual("first");
2062
+ expect(first.attr("value")).toEqual("1");
2063
+ const forth = jqLite(element.find("option")[3]);
2064
+ expect(forth.text()).toEqual("forth");
2065
+ expect(forth.attr("value")).toEqual("4");
2066
+
2067
+ scope.$apply(() => {
2068
+ scope.selected = scope.values[3];
2069
+ });
2070
+
2071
+ expect(element.val()).toEqual("4");
2072
+ });
2073
+
2074
+ it("should bind to scope value through expression", () => {
2075
+ createSelect({
2076
+ "ng-model": "selected",
2077
+ "ng-options": "item.id as item.name for item in values",
2078
+ });
2079
+
2080
+ scope.$apply(() => {
2081
+ scope.values = [
2082
+ { id: 10, name: "A" },
2083
+ { id: 20, name: "B" },
2084
+ ];
2085
+ scope.selected = scope.values[0].id;
2086
+ });
2087
+
2088
+ expect(element).toEqualSelectValue(scope.selected);
2089
+
2090
+ scope.$apply(() => {
2091
+ scope.selected = scope.values[1].id;
2092
+ });
2093
+
2094
+ expect(element).toEqualSelectValue(scope.selected);
2095
+ });
2096
+
2097
+ it("should update options in the DOM", () => {
2098
+ compile(
2099
+ '<select ng-model="selected" ng-options="item.id as item.name for item in values"></select>',
2100
+ );
2101
+
2102
+ scope.$apply(() => {
2103
+ scope.values = [
2104
+ { id: 10, name: "A" },
2105
+ { id: 20, name: "B" },
2106
+ ];
2107
+ scope.selected = scope.values[0].id;
2108
+ });
2109
+
2110
+ scope.$apply(() => {
2111
+ scope.values[0].name = "C";
2112
+ });
2113
+
2114
+ const options = element.find("option");
2115
+ expect(options.length).toEqual(2);
2116
+ expect(options.eq(0)).toEqualOption(10, "C");
2117
+ expect(options.eq(1)).toEqualOption(20, "B");
2118
+ });
2119
+
2120
+ it("should update options in the DOM from object source", () => {
2121
+ compile(
2122
+ '<select ng-model="selected" ng-options="val.id as val.name for (key, val) in values"></select>',
2123
+ );
2124
+
2125
+ scope.$apply(() => {
2126
+ scope.values = { a: { id: 10, name: "A" }, b: { id: 20, name: "B" } };
2127
+ scope.selected = scope.values.a.id;
2128
+ });
2129
+
2130
+ scope.$apply(() => {
2131
+ scope.values.a.name = "C";
2132
+ });
2133
+
2134
+ const options = element.find("option");
2135
+ expect(options.length).toEqual(2);
2136
+ expect(options.eq(0)).toEqualOption(10, "C");
2137
+ expect(options.eq(1)).toEqualOption(20, "B");
2138
+ });
2139
+
2140
+ it("should bind to object key", () => {
2141
+ createSelect({
2142
+ "ng-model": "selected",
2143
+ "ng-options": "key as value for (key, value) in object",
2144
+ });
2145
+
2146
+ scope.$apply(() => {
2147
+ scope.object = { red: "FF0000", green: "00FF00", blue: "0000FF" };
2148
+ scope.selected = "green";
2149
+ });
2150
+
2151
+ expect(element).toEqualSelectValue(scope.selected);
2152
+
2153
+ scope.$apply(() => {
2154
+ scope.selected = "blue";
2155
+ });
2156
+
2157
+ expect(element).toEqualSelectValue(scope.selected);
2158
+ });
2159
+
2160
+ it("should bind to object value", () => {
2161
+ createSelect({
2162
+ "ng-model": "selected",
2163
+ "ng-options": "value as key for (key, value) in object",
2164
+ });
2165
+
2166
+ scope.$apply(() => {
2167
+ scope.object = { red: "FF0000", green: "00FF00", blue: "0000FF" };
2168
+ scope.selected = "00FF00";
2169
+ });
2170
+
2171
+ expect(element).toEqualSelectValue(scope.selected);
2172
+
2173
+ scope.$apply(() => {
2174
+ scope.selected = "0000FF";
2175
+ });
2176
+
2177
+ expect(element).toEqualSelectValue(scope.selected);
2178
+ });
2179
+
2180
+ it("should bind to object disabled", () => {
2181
+ scope.selected = 30;
2182
+ scope.options = [
2183
+ { name: "white", value: "#FFFFFF" },
2184
+ { name: "one", value: 1, unavailable: true },
2185
+ { name: "notTrue", value: false },
2186
+ { name: "thirty", value: 30, unavailable: false },
2187
+ ];
2188
+ createSelect({
2189
+ "ng-options":
2190
+ "o.value as o.name disable when o.unavailable for o in options",
2191
+ "ng-model": "selected",
2192
+ });
2193
+
2194
+ let options = element.find("option");
2195
+
2196
+ expect(scope.options[1].unavailable).toEqual(true);
2197
+ expect(options.eq(1).prop("disabled")).toEqual(true);
2198
+
2199
+ scope.$apply(() => {
2200
+ scope.options[1].unavailable = false;
2201
+ });
2202
+
2203
+ options = element.find("option");
2204
+
2205
+ expect(scope.options[1].unavailable).toEqual(false);
2206
+ expect(options.eq(1).prop("disabled")).toEqual(false);
2207
+ });
2208
+
2209
+ it("should insert the unknown option if bound to null", () => {
2210
+ createSingleSelect();
2211
+
2212
+ scope.$apply(() => {
2213
+ scope.values = [{ name: "A" }];
2214
+ scope.selected = null;
2215
+ });
2216
+
2217
+ expect(element.find("option").length).toEqual(2);
2218
+ expect(element.val()).toEqual("?");
2219
+ expect(jqLite(element.find("option")[0]).val()).toEqual("?");
2220
+
2221
+ scope.$apply(() => {
2222
+ scope.selected = scope.values[0];
2223
+ });
2224
+
2225
+ expect(element).toEqualSelectValue(scope.selected);
2226
+ expect(element.find("option").length).toEqual(1);
2227
+ });
2228
+
2229
+ it("should select the provided empty option if bound to null", () => {
2230
+ createSingleSelect(true);
2231
+
2232
+ scope.$apply(() => {
2233
+ scope.values = [{ name: "A" }];
2234
+ scope.selected = null;
2235
+ });
2236
+
2237
+ expect(element.find("option").length).toEqual(2);
2238
+ expect(element.val()).toEqual("");
2239
+ expect(jqLite(element.find("option")[0]).val()).toEqual("");
2240
+
2241
+ scope.$apply(() => {
2242
+ scope.selected = scope.values[0];
2243
+ });
2244
+
2245
+ expect(element).toEqualSelectValue(scope.selected);
2246
+ expect(jqLite(element.find("option")[0]).val()).toEqual("");
2247
+ expect(element.find("option").length).toEqual(2);
2248
+ });
2249
+
2250
+ it("should reuse blank option if bound to null", () => {
2251
+ createSingleSelect(true);
2252
+
2253
+ scope.$apply(() => {
2254
+ scope.values = [{ name: "A" }];
2255
+ scope.selected = null;
2256
+ });
2257
+
2258
+ expect(element.find("option").length).toEqual(2);
2259
+ expect(element.val()).toEqual("");
2260
+ expect(jqLite(element.find("option")[0]).val()).toEqual("");
2261
+
2262
+ scope.$apply(() => {
2263
+ scope.selected = scope.values[0];
2264
+ });
2265
+
2266
+ expect(element).toEqualSelectValue(scope.selected);
2267
+ expect(element.find("option").length).toEqual(2);
2268
+ });
2269
+
2270
+ it("should not insert a blank option if one of the options maps to null", () => {
2271
+ createSelect({
2272
+ "ng-model": "myColor",
2273
+ "ng-options": "color.shade as color.name for color in colors",
2274
+ });
2275
+
2276
+ scope.$apply(() => {
2277
+ scope.colors = [
2278
+ { name: "nothing", shade: null },
2279
+ { name: "red", shade: "dark" },
2280
+ ];
2281
+ scope.myColor = null;
2282
+ });
2283
+
2284
+ expect(element.find("option").length).toEqual(2);
2285
+ expect(element.find("option").eq(0)).toEqualOption(null);
2286
+ expect(element.val()).not.toEqualUnknownValue(null);
2287
+ expect(element.find("option").eq(0)).not.toEqualUnknownOption(null);
2288
+ });
2289
+
2290
+ it("should insert a unknown option if bound to something not in the list", () => {
2291
+ createSingleSelect();
2292
+
2293
+ scope.$apply(() => {
2294
+ scope.values = [{ name: "A" }];
2295
+ scope.selected = {};
2296
+ });
2297
+
2298
+ expect(element.find("option").length).toEqual(2);
2299
+ expect(element.val()).toEqualUnknownValue(scope.selected);
2300
+ expect(element.find("option").eq(0)).toEqualUnknownOption(scope.selected);
2301
+
2302
+ scope.$apply(() => {
2303
+ scope.selected = scope.values[0];
2304
+ });
2305
+
2306
+ expect(element).toEqualSelectValue(scope.selected);
2307
+ expect(element.find("option").length).toEqual(1);
2308
+ });
2309
+
2310
+ it(
2311
+ "should insert and select temporary unknown option when no options-model match, empty " +
2312
+ "option is present and model is defined",
2313
+ () => {
2314
+ scope.selected = "C";
2315
+ scope.values = [{ name: "A" }, { name: "B" }];
2316
+ createSingleSelect(true);
2317
+
2318
+ expect(element[0].value).toBe("?");
2319
+ expect(element[0].length).toBe(4);
2320
+
2321
+ scope.$apply("selected = values[1]");
2322
+
2323
+ expect(element[0].value).not.toBe("");
2324
+ expect(element[0].length).toBe(3);
2325
+ },
2326
+ );
2327
+
2328
+ it('should select correct input if previously selected option was "?"', () => {
2329
+ createSingleSelect();
2330
+
2331
+ scope.$apply(() => {
2332
+ scope.values = [{ name: "A" }, { name: "B" }];
2333
+ scope.selected = {};
2334
+ });
2335
+
2336
+ expect(element.find("option").length).toEqual(3);
2337
+ expect(element.val()).toEqualUnknownValue();
2338
+ expect(element.find("option").eq(0)).toEqualUnknownOption();
2339
+
2340
+ setSelectValue(element, 1);
2341
+
2342
+ expect(element.find("option").length).toEqual(2);
2343
+ expect(element).toEqualSelectValue(scope.selected);
2344
+ expect(element.find("option").eq(0).prop("selected")).toBeTruthy();
2345
+ });
2346
+
2347
+ it("should remove unknown option when empty option exists and model is undefined", () => {
2348
+ scope.selected = "C";
2349
+ scope.values = [{ name: "A" }, { name: "B" }];
2350
+ createSingleSelect(true);
2351
+
2352
+ expect(element[0].value).toBe("?");
2353
+
2354
+ scope.selected = undefined;
2355
+ scope.$digest();
2356
+
2357
+ expect(element[0].value).toBe("");
2358
+ });
2359
+
2360
+ it("should use exact same values as values in scope with one-time bindings", () => {
2361
+ scope.values = [{ name: "A" }, { name: "B" }];
2362
+ scope.selected = scope.values[0];
2363
+ createSelect({
2364
+ "ng-model": "selected",
2365
+ "ng-options": "value.name for value in ::values",
2366
+ });
2367
+
2368
+ setSelectValue(element, 1);
2369
+
2370
+ expect(scope.selected).toBe(scope.values[1]);
2371
+ });
2372
+
2373
+ it('should ensure that at least one option element has the "selected" attribute', () => {
2374
+ createSelect({
2375
+ "ng-model": "selected",
2376
+ "ng-options": "item.id as item.name for item in values",
2377
+ });
2378
+
2379
+ scope.$apply(() => {
2380
+ scope.values = [
2381
+ { id: 10, name: "A" },
2382
+ { id: 20, name: "B" },
2383
+ ];
2384
+ });
2385
+ expect(element.val()).toEqualUnknownValue();
2386
+ expect(element.find("option").eq(0).attr("selected")).toEqual("selected");
2387
+
2388
+ scope.$apply(() => {
2389
+ scope.selected = 10;
2390
+ });
2391
+ // Here the ? option should disappear and the first real option should have selected attribute
2392
+ expect(element).toEqualSelectValue(scope.selected);
2393
+ expect(element.find("option").eq(0).attr("selected")).toEqual("selected");
2394
+
2395
+ // Here the selected value is changed and we change the selected attribute
2396
+ scope.$apply(() => {
2397
+ scope.selected = 20;
2398
+ });
2399
+ expect(element).toEqualSelectValue(scope.selected);
2400
+ expect(element.find("option").eq(1).attr("selected")).toEqual("selected");
2401
+
2402
+ scope.$apply(() => {
2403
+ scope.values.push({ id: 30, name: "C" });
2404
+ });
2405
+ expect(element).toEqualSelectValue(scope.selected);
2406
+ expect(element.find("option").eq(1).attr("selected")).toEqual("selected");
2407
+
2408
+ // Here the ? option should reappear and have selected attribute
2409
+ scope.$apply(() => {
2410
+ scope.selected = undefined;
2411
+ });
2412
+ expect(element.val()).toEqualUnknownValue();
2413
+ expect(element.find("option").eq(0).attr("selected")).toEqual("selected");
2414
+ });
2415
+
2416
+ it("should select the correct option for selectAs and falsy values", () => {
2417
+ scope.values = [
2418
+ { value: 0, label: "zero" },
2419
+ { value: 1, label: "one" },
2420
+ ];
2421
+ scope.selected = "";
2422
+ createSelect({
2423
+ "ng-model": "selected",
2424
+ "ng-options": "option.value as option.label for option in values",
2425
+ });
2426
+
2427
+ const option = element.find("option").eq(0);
2428
+ expect(option).toEqualUnknownOption();
2429
+ });
2430
+
2431
+ it("should update the model if the selected option is removed", () => {
2432
+ scope.values = [
2433
+ { value: 0, label: "zero" },
2434
+ { value: 1, label: "one" },
2435
+ ];
2436
+ scope.selected = 1;
2437
+ createSelect({
2438
+ "ng-model": "selected",
2439
+ "ng-options": "option.value as option.label for option in values",
2440
+ });
2441
+ expect(element).toEqualSelectValue(1);
2442
+
2443
+ // Check after initial option update
2444
+ scope.$apply(() => {
2445
+ scope.values.pop();
2446
+ });
2447
+
2448
+ expect(element.val()).toEqual("?");
2449
+ expect(scope.selected).toEqual(null);
2450
+
2451
+ // Check after model change
2452
+ scope.$apply(() => {
2453
+ scope.selected = 0;
2454
+ });
2455
+
2456
+ expect(element).toEqualSelectValue(0);
2457
+
2458
+ scope.$apply(() => {
2459
+ scope.values.pop();
2460
+ });
2461
+
2462
+ expect(element.val()).toEqual("?");
2463
+ expect(scope.selected).toEqual(null);
2464
+ });
2465
+
2466
+ it("should update the model if all the selected (multiple) options are removed", () => {
2467
+ scope.values = [
2468
+ { value: 0, label: "zero" },
2469
+ { value: 1, label: "one" },
2470
+ { value: 2, label: "two" },
2471
+ ];
2472
+ scope.selected = [1, 2];
2473
+ createSelect({
2474
+ "ng-model": "selected",
2475
+ multiple: true,
2476
+ "ng-options": "option.value as option.label for option in values",
2477
+ });
2478
+
2479
+ expect(element).toEqualSelectValue([1, 2], true);
2480
+
2481
+ // Check after initial option update
2482
+ scope.$apply(() => {
2483
+ scope.values.pop();
2484
+ });
2485
+
2486
+ expect(element).toEqualSelectValue([1], true);
2487
+ expect(scope.selected).toEqual([1]);
2488
+
2489
+ scope.$apply(() => {
2490
+ scope.values.pop();
2491
+ });
2492
+
2493
+ expect(element).toEqualSelectValue([], true);
2494
+ expect(scope.selected).toEqual([]);
2495
+
2496
+ // Check after model change
2497
+ scope.$apply(() => {
2498
+ scope.selected = [0];
2499
+ });
2500
+
2501
+ expect(element).toEqualSelectValue([0], true);
2502
+
2503
+ scope.$apply(() => {
2504
+ scope.values.pop();
2505
+ });
2506
+
2507
+ expect(element).toEqualSelectValue([], true);
2508
+ expect(scope.selected).toEqual([]);
2509
+ });
2510
+ });
2511
+
2512
+ describe("empty option", () => {
2513
+ it("should be compiled as template, be watched and updated", () => {
2514
+ let option;
2515
+ createSingleSelect('<option value="">blank is {{blankVal}}</option>');
2516
+
2517
+ scope.$apply(() => {
2518
+ scope.blankVal = "so blank";
2519
+ scope.values = [{ name: "A" }];
2520
+ });
2521
+
2522
+ // check blank option is first and is compiled
2523
+ expect(element.find("option").length).toBe(2);
2524
+ option = element.find("option").eq(0);
2525
+ expect(option.val()).toBe("");
2526
+ expect(option.text()).toBe("blank is so blank");
2527
+
2528
+ scope.$apply(() => {
2529
+ scope.blankVal = "not so blank";
2530
+ });
2531
+
2532
+ // check blank option is first and is compiled
2533
+ expect(element.find("option").length).toBe(2);
2534
+ option = element.find("option").eq(0);
2535
+ expect(option.val()).toBe("");
2536
+ expect(option.text()).toBe("blank is not so blank");
2537
+ });
2538
+
2539
+ it("should support binding via ngBindTemplate directive", () => {
2540
+ let option;
2541
+ createSingleSelect(
2542
+ '<option value="" ng-bind-template="blank is {{blankVal}}"></option>',
2543
+ );
2544
+
2545
+ scope.$apply(() => {
2546
+ scope.blankVal = "so blank";
2547
+ scope.values = [{ name: "A" }];
2548
+ });
2549
+
2550
+ // check blank option is first and is compiled
2551
+ expect(element.find("option").length).toBe(2);
2552
+ option = element.find("option").eq(0);
2553
+ expect(option.val()).toBe("");
2554
+ expect(option.text()).toBe("blank is so blank");
2555
+ });
2556
+
2557
+ it("should support binding via ngBind attribute", () => {
2558
+ let option;
2559
+ createSingleSelect('<option value="" ng-bind="blankVal"></option>');
2560
+
2561
+ scope.$apply(() => {
2562
+ scope.blankVal = "is blank";
2563
+ scope.values = [{ name: "A" }];
2564
+ });
2565
+
2566
+ // check blank option is first and is compiled
2567
+ expect(element.find("option").length).toBe(2);
2568
+ option = element.find("option").eq(0);
2569
+ expect(option.val()).toBe("");
2570
+ expect(option.text()).toBe("is blank");
2571
+ });
2572
+
2573
+ it("should be ignored when it has no value attribute", () => {
2574
+ // The option value is set to the textContent if there's no value attribute,
2575
+ // so in that case it doesn't count as a blank option
2576
+ createSingleSelect("<option>--select--</option>");
2577
+ scope.$apply(() => {
2578
+ scope.values = [{ name: "A" }, { name: "B" }, { name: "C" }];
2579
+ });
2580
+
2581
+ const options = element.find("option");
2582
+
2583
+ expect(options.eq(0)).toEqualUnknownOption();
2584
+ expect(options.eq(1)).toEqualOption(scope.values[0], "A");
2585
+ expect(options.eq(2)).toEqualOption(scope.values[1], "B");
2586
+ expect(options.eq(3)).toEqualOption(scope.values[2], "C");
2587
+ });
2588
+
2589
+ it("should be rendered with the attributes preserved", () => {
2590
+ let option;
2591
+ createSingleSelect(
2592
+ '<option value="" class="coyote" id="road-runner" ' +
2593
+ 'custom-attr="custom-attr">{{blankVal}}</option>',
2594
+ );
2595
+
2596
+ scope.$apply(() => {
2597
+ scope.blankVal = "is blank";
2598
+ });
2599
+
2600
+ // check blank option is first and is compiled
2601
+ option = element.find("option").eq(0);
2602
+ expect(option[0].classList.contains("coyote")).toBeTruthy();
2603
+ expect(option.attr("id")).toBe("road-runner");
2604
+ expect(option.attr("custom-attr")).toBe("custom-attr");
2605
+ });
2606
+
2607
+ it("should be selected, if it is available and no other option is selected", () => {
2608
+ // selectedIndex is used here because jqLite incorrectly reports element.val()
2609
+ scope.$apply(() => {
2610
+ scope.values = [{ name: "A" }];
2611
+ });
2612
+ createSingleSelect(true);
2613
+ // ensure the first option (the blank option) is selected
2614
+ expect(element[0].selectedIndex).toEqual(0);
2615
+ scope.$digest();
2616
+ // ensure the option has not changed following the digest
2617
+ expect(element[0].selectedIndex).toEqual(0);
2618
+ });
2619
+
2620
+ it("should be selectable if select is multiple", () => {
2621
+ createMultiSelect(true);
2622
+
2623
+ // select the empty option
2624
+ setSelectValue(element, 0);
2625
+
2626
+ // ensure selection and correct binding
2627
+ expect(element[0].selectedIndex).toEqual(0);
2628
+ expect(scope.selected).toEqual([]);
2629
+ });
2630
+
2631
+ it("should be possible to use ngIf in the blank option", () => {
2632
+ let option;
2633
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2634
+
2635
+ scope.$apply(() => {
2636
+ scope.values = [{ name: "A" }];
2637
+ scope.isBlank = true;
2638
+ });
2639
+
2640
+ expect(element[0].value).toBe("");
2641
+
2642
+ scope.$apply("isBlank = false");
2643
+
2644
+ expect(element[0].value).toBe("?");
2645
+
2646
+ scope.$apply("isBlank = true");
2647
+
2648
+ expect(element[0].value).toBe("");
2649
+ });
2650
+
2651
+ it("should be possible to use ngIf in the blank option when values are available upon linking", () => {
2652
+ let options;
2653
+
2654
+ scope.values = [{ name: "A" }];
2655
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2656
+
2657
+ scope.$apply("isBlank = true");
2658
+
2659
+ options = element.find("option");
2660
+ expect(options.length).toBe(2);
2661
+ expect(options.eq(0).val()).toBe("");
2662
+ expect(options.eq(0).text()).toBe("blank");
2663
+
2664
+ scope.$apply("isBlank = false");
2665
+
2666
+ expect(element[0].value).toBe("?");
2667
+ });
2668
+
2669
+ it("should select the correct option after linking when the ngIf expression is initially falsy", () => {
2670
+ scope.values = [{ name: "black" }, { name: "white" }, { name: "red" }];
2671
+ scope.selected = scope.values[2];
2672
+
2673
+ expect(() => {
2674
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2675
+ scope.$apply();
2676
+ }).not.toThrow();
2677
+
2678
+ expect(element.find("option")[2].selected).toBe(true);
2679
+ expect(linkLog).toEqual(["linkNgOptions"]);
2680
+ });
2681
+
2682
+ it('should add / remove the "selected" attribute on empty option which has an initially falsy ngIf expression', () => {
2683
+ scope.values = [{ name: "black" }, { name: "white" }, { name: "red" }];
2684
+ scope.selected = scope.values[2];
2685
+
2686
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2687
+ scope.$apply();
2688
+
2689
+ expect(element.find("option")[2].selected).toBe(true);
2690
+
2691
+ scope.$apply("isBlank = true");
2692
+ expect(element.find("option")[0].value).toBe("");
2693
+ expect(element.find("option")[0].selected).toBe(false);
2694
+
2695
+ scope.$apply("selected = null");
2696
+ expect(element.find("option")[0].value).toBe("");
2697
+ expect(element.find("option")[0].selected).toBe(true);
2698
+
2699
+ scope.selected = scope.values[1];
2700
+ scope.$apply();
2701
+ expect(element.find("option")[0].value).toBe("");
2702
+ expect(element.find("option")[0].selected).toBe(false);
2703
+ expect(element.find("option")[2].selected).toBe(true);
2704
+ });
2705
+
2706
+ it('should add / remove the "selected" attribute on empty option which has an initially truthy ngIf expression when no option is selected', () => {
2707
+ scope.values = [{ name: "black" }, { name: "white" }, { name: "red" }];
2708
+ scope.isBlank = true;
2709
+
2710
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2711
+ scope.$apply();
2712
+
2713
+ expect(element.find("option")[0].value).toBe("");
2714
+ expect(element.find("option")[0].selected).toBe(true);
2715
+ scope.selected = scope.values[2];
2716
+ scope.$apply();
2717
+ expect(element.find("option")[0].selected).toBe(false);
2718
+ expect(element.find("option")[3].selected).toBe(true);
2719
+ });
2720
+
2721
+ it('should add the "selected" attribute on empty option which has an initially falsy ngIf expression when no option is selected', () => {
2722
+ scope.values = [{ name: "black" }, { name: "white" }, { name: "red" }];
2723
+
2724
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
2725
+ scope.$apply();
2726
+
2727
+ expect(element.find("option")[0].selected).toBe(false);
2728
+
2729
+ scope.isBlank = true;
2730
+ scope.$apply();
2731
+
2732
+ expect(element.find("option")[0].value).toBe("");
2733
+ expect(element.find("option")[0].selected).toBe(true);
2734
+ expect(element.find("option")[1].selected).toBe(false);
2735
+ });
2736
+
2737
+ it("should not throw when a directive compiles the blank option before ngOptions is linked", () => {
2738
+ expect(() => {
2739
+ createSelect(
2740
+ {
2741
+ "o-compile-contents": "",
2742
+ name: "select",
2743
+ "ng-model": "value",
2744
+ "ng-options": "item for item in items",
2745
+ },
2746
+ true,
2747
+ );
2748
+ }).not.toThrow();
2749
+
2750
+ expect(linkLog).toEqual(["linkCompileContents", "linkNgOptions"]);
2751
+ });
2752
+
2753
+ it("should not throw with a directive that replaces", () => {
2754
+ let $templateCache = injector.get("$templateCache");
2755
+ $templateCache.put(
2756
+ "select_template.html",
2757
+ '<select ng-options="option as option for option in selectable_options"> <option value="">This is a test</option> </select>',
2758
+ );
2759
+
2760
+ scope.options = ["a", "b", "c", "d"];
2761
+
2762
+ expect(() => {
2763
+ element = $compile(
2764
+ '<custom-select ng-model="value" options="options"></custom-select>',
2765
+ )(scope);
2766
+ scope.$digest();
2767
+ }).not.toThrow();
2768
+
2769
+ dealoc(element);
2770
+ });
2771
+ });
2772
+
2773
+ describe("on change", () => {
2774
+ it("should update model on change", () => {
2775
+ createSingleSelect();
2776
+
2777
+ scope.$apply(() => {
2778
+ scope.values = [{ name: "A" }, { name: "B" }];
2779
+ scope.selected = scope.values[0];
2780
+ });
2781
+
2782
+ expect(element).toEqualSelectValue(scope.selected);
2783
+
2784
+ setSelectValue(element, 1);
2785
+ expect(scope.selected).toEqual(scope.values[1]);
2786
+ });
2787
+
2788
+ it("should update model on change through expression", () => {
2789
+ createSelect({
2790
+ "ng-model": "selected",
2791
+ "ng-options": "item.id as item.name for item in values",
2792
+ });
2793
+
2794
+ scope.$apply(() => {
2795
+ scope.values = [
2796
+ { id: 10, name: "A" },
2797
+ { id: 20, name: "B" },
2798
+ ];
2799
+ scope.selected = scope.values[0].id;
2800
+ });
2801
+
2802
+ expect(element).toEqualSelectValue(scope.selected);
2803
+
2804
+ setSelectValue(element, 1);
2805
+ expect(scope.selected).toEqual(scope.values[1].id);
2806
+ });
2807
+
2808
+ it("should update model to null on change", () => {
2809
+ createSingleSelect(true);
2810
+
2811
+ scope.$apply(() => {
2812
+ scope.values = [{ name: "A" }, { name: "B" }];
2813
+ scope.selected = scope.values[0];
2814
+ });
2815
+
2816
+ element.val("");
2817
+ browserTrigger(element, "change");
2818
+ expect(scope.selected).toEqual(null);
2819
+ });
2820
+
2821
+ // Regression https://github.com/angular/angular.js/issues/7855
2822
+ it("should update the model with ng-change", () => {
2823
+ createSelect({
2824
+ "ng-change": "change()",
2825
+ "ng-model": "selected",
2826
+ "ng-options": "value for value in values",
2827
+ });
2828
+
2829
+ scope.$apply(() => {
2830
+ scope.values = ["A", "B"];
2831
+ scope.selected = "A";
2832
+ });
2833
+
2834
+ scope.change = function () {
2835
+ scope.selected = "A";
2836
+ };
2837
+
2838
+ element.find("option")[1].selected = true;
2839
+
2840
+ browserTrigger(element, "change");
2841
+ expect(element.find("option")[0].selected).toBeTruthy();
2842
+ expect(scope.selected).toEqual("A");
2843
+ });
2844
+ });
2845
+
2846
+ describe("disabled blank", () => {
2847
+ it("should select disabled blank by default", () => {
2848
+ const html =
2849
+ '<select ng-model="someModel" ng-options="c for c in choices">' +
2850
+ '<option value="" disabled>Choose One</option>' +
2851
+ "</select>";
2852
+ scope.$apply(() => {
2853
+ scope.choices = ["A", "B", "C"];
2854
+ });
2855
+
2856
+ compile(html);
2857
+
2858
+ const options = element.find("option");
2859
+ const optionToSelect = options.eq(0);
2860
+ expect(optionToSelect.text()).toBe("Choose One");
2861
+ expect(optionToSelect.prop("selected")).toBe(true);
2862
+ expect(element[0].value).toBe("");
2863
+
2864
+ dealoc(element);
2865
+ });
2866
+
2867
+ it("should select disabled blank by default when select is required", () => {
2868
+ const html =
2869
+ '<select ng-model="someModel" ng-options="c for c in choices" required>' +
2870
+ '<option value="" disabled>Choose One</option>' +
2871
+ "</select>";
2872
+ scope.$apply(() => {
2873
+ scope.choices = ["A", "B", "C"];
2874
+ });
2875
+
2876
+ compile(html);
2877
+
2878
+ const options = element.find("option");
2879
+ const optionToSelect = options.eq(0);
2880
+ expect(optionToSelect.text()).toBe("Choose One");
2881
+ expect(optionToSelect.prop("selected")).toBe(true);
2882
+ expect(element[0].value).toBe("");
2883
+
2884
+ dealoc(element);
2885
+ });
2886
+ });
2887
+
2888
+ describe("select-many", () => {
2889
+ it("should read multiple selection", () => {
2890
+ createMultiSelect();
2891
+
2892
+ scope.$apply(() => {
2893
+ scope.values = [{ name: "A" }, { name: "B" }];
2894
+ scope.selected = [];
2895
+ });
2896
+
2897
+ expect(element.find("option").length).toEqual(2);
2898
+ expect(element.find("option")[0].selected).toBeFalsy();
2899
+ expect(element.find("option")[1].selected).toBeFalsy();
2900
+
2901
+ scope.$apply(() => {
2902
+ scope.selected.push(scope.values[1]);
2903
+ });
2904
+
2905
+ expect(element.find("option").length).toEqual(2);
2906
+ expect(element.find("option")[0].selected).toBeFalsy();
2907
+ expect(element.find("option")[1].selected).toBeTruthy();
2908
+
2909
+ scope.$apply(() => {
2910
+ scope.selected.push(scope.values[0]);
2911
+ });
2912
+
2913
+ expect(element.find("option").length).toEqual(2);
2914
+ expect(element.find("option")[0].selected).toBeTruthy();
2915
+ expect(element.find("option")[1].selected).toBeTruthy();
2916
+ });
2917
+
2918
+ it("should update model on change", () => {
2919
+ createMultiSelect();
2920
+
2921
+ scope.$apply(() => {
2922
+ scope.values = [{ name: "A" }, { name: "B" }];
2923
+ scope.selected = [];
2924
+ });
2925
+
2926
+ element.find("option")[0].selected = true;
2927
+
2928
+ browserTrigger(element, "change");
2929
+ expect(scope.selected).toEqual([scope.values[0]]);
2930
+ });
2931
+
2932
+ it("should select from object", () => {
2933
+ createSelect({
2934
+ "ng-model": "selected",
2935
+ multiple: true,
2936
+ "ng-options": "key as value for (key,value) in values",
2937
+ });
2938
+ scope.values = { 0: "A", 1: "B" };
2939
+
2940
+ scope.selected = ["1"];
2941
+ scope.$digest();
2942
+ expect(element.find("option")[1].selected).toBe(true);
2943
+
2944
+ element.find("option")[0].selected = true;
2945
+ browserTrigger(element, "change");
2946
+ expect(scope.selected).toEqual(["0", "1"]);
2947
+
2948
+ element.find("option")[1].selected = false;
2949
+ browserTrigger(element, "change");
2950
+ expect(scope.selected).toEqual(["0"]);
2951
+ });
2952
+
2953
+ it("should deselect all options when model is emptied", () => {
2954
+ createMultiSelect();
2955
+ scope.$apply(() => {
2956
+ scope.values = [{ name: "A" }, { name: "B" }];
2957
+ scope.selected = [scope.values[0]];
2958
+ });
2959
+ expect(element.find("option")[0].selected).toEqual(true);
2960
+
2961
+ scope.$apply(() => {
2962
+ scope.selected.pop();
2963
+ });
2964
+
2965
+ expect(element.find("option")[0].selected).toEqual(false);
2966
+ });
2967
+
2968
+ // Support: Safari 9+
2969
+ // This test relies defining a getter/setter `selected` property on either `<option>` elements
2970
+ // or their prototype. Some browsers (including Safari 9) are very flakey when the
2971
+ // getter/setter is not defined on the prototype (probably due to some bug). On Safari 9, the
2972
+ // getter/setter that is already defined on the `<option>` element's prototype is not
2973
+ // configurable, so we can't overwrite it with our spy.
2974
+ if (
2975
+ !/\b(9|\d{2})(?:\.\d+)+[\s\S]*safari/i.test(window.navigator.userAgent)
2976
+ ) {
2977
+ it("should not re-set the `selected` property if it already has the correct value", () => {
2978
+ scope.values = [{ name: "A" }, { name: "B" }];
2979
+ createMultiSelect();
2980
+
2981
+ const options = element.find("option");
2982
+ const optionsSetSelected = [];
2983
+ const _selected = [];
2984
+
2985
+ // Set up spies
2986
+ const optionProto = Object.getPrototypeOf(options[0]);
2987
+ const originalSelectedDescriptor =
2988
+ isFunction(Object.getOwnPropertyDescriptor) &&
2989
+ Object.getOwnPropertyDescriptor(optionProto, "selected");
2990
+ const addSpiesOnProto =
2991
+ originalSelectedDescriptor && originalSelectedDescriptor.configurable;
2992
+
2993
+ forEach(options, (option, i) => {
2994
+ const setSelected = function (value) {
2995
+ _selected[i] = value;
2996
+ };
2997
+ optionsSetSelected[i] = jasmine
2998
+ .createSpy(`optionSetSelected${i}`)
2999
+ .and.callFake(setSelected);
3000
+ setSelected(option.selected);
3001
+ });
3002
+
3003
+ if (!addSpiesOnProto) {
3004
+ forEach(options, (option, i) => {
3005
+ Object.defineProperty(option, "selected", {
3006
+ get() {
3007
+ return _selected[i];
3008
+ },
3009
+ set: optionsSetSelected[i],
3010
+ });
3011
+ });
3012
+ } else {
3013
+ // Support: Firefox 54+
3014
+ // We cannot use the above (simpler) method on all browsers because of Firefox 54+, which
3015
+ // is very flaky when the getter/setter property is defined on the element itself and not
3016
+ // the prototype. (Possibly the result of some (buggy?) optimization.)
3017
+ const getSelected = function (index) {
3018
+ return _selected[index];
3019
+ };
3020
+ const setSelected = function (index, value) {
3021
+ optionsSetSelected[index](value);
3022
+ };
3023
+ const getSelectedOriginal = function (option) {
3024
+ return originalSelectedDescriptor.get.call(option);
3025
+ };
3026
+ const setSelectedOriginal = function (option, value) {
3027
+ originalSelectedDescriptor.set.call(option, value);
3028
+ };
3029
+ const getIndexAndCall = function (
3030
+ option,
3031
+ foundFn,
3032
+ notFoundFn,
3033
+ value,
3034
+ ) {
3035
+ for (let i = 0, ii = options.length; i < ii; ++i) {
3036
+ if (options[i] === option) return foundFn(i, value);
3037
+ }
3038
+ return notFoundFn(option, value);
3039
+ };
3040
+
3041
+ Object.defineProperty(optionProto, "selected", {
3042
+ get() {
3043
+ return getIndexAndCall(this, getSelected, getSelectedOriginal);
3044
+ },
3045
+ set(value) {
3046
+ return getIndexAndCall(
3047
+ this,
3048
+ setSelected,
3049
+ setSelectedOriginal,
3050
+ value,
3051
+ );
3052
+ },
3053
+ });
3054
+ }
3055
+
3056
+ // Select `optionA`
3057
+ scope.$apply("selected = [values[0]]");
3058
+
3059
+ expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
3060
+ expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3061
+ expect(options[0].selected).toBe(true);
3062
+ expect(options[1].selected).toBe(false);
3063
+ optionsSetSelected[0].calls.reset();
3064
+ optionsSetSelected[1].calls.reset();
3065
+
3066
+ // Select `optionB` (`optionA` remains selected)
3067
+ scope.$apply("selected.push(values[1])");
3068
+
3069
+ expect(optionsSetSelected[0]).not.toHaveBeenCalled();
3070
+ expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(true);
3071
+ expect(options[0].selected).toBe(true);
3072
+ expect(options[1].selected).toBe(true);
3073
+ optionsSetSelected[0].calls.reset();
3074
+ optionsSetSelected[1].calls.reset();
3075
+
3076
+ // Unselect `optionA` (`optionB` remains selected)
3077
+ scope.$apply("selected.shift()");
3078
+
3079
+ expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
3080
+ expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3081
+ expect(options[0].selected).toBe(false);
3082
+ expect(options[1].selected).toBe(true);
3083
+ optionsSetSelected[0].calls.reset();
3084
+ optionsSetSelected[1].calls.reset();
3085
+
3086
+ // Reselect `optionA` (`optionB` remains selected)
3087
+ scope.$apply("selected.push(values[0])");
3088
+
3089
+ expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(true);
3090
+ expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3091
+ expect(options[0].selected).toBe(true);
3092
+ expect(options[1].selected).toBe(true);
3093
+ optionsSetSelected[0].calls.reset();
3094
+ optionsSetSelected[1].calls.reset();
3095
+
3096
+ // Unselect `optionB` (`optionA` remains selected)
3097
+ scope.$apply("selected.shift()");
3098
+
3099
+ expect(optionsSetSelected[0]).not.toHaveBeenCalled();
3100
+ expect(optionsSetSelected[1]).toHaveBeenCalledOnceWith(false);
3101
+ expect(options[0].selected).toBe(true);
3102
+ expect(options[1].selected).toBe(false);
3103
+ optionsSetSelected[0].calls.reset();
3104
+ optionsSetSelected[1].calls.reset();
3105
+
3106
+ // Unselect `optionA`
3107
+ scope.$apply("selected.length = 0");
3108
+
3109
+ expect(optionsSetSelected[0]).toHaveBeenCalledOnceWith(false);
3110
+ expect(optionsSetSelected[1]).not.toHaveBeenCalled();
3111
+ expect(options[0].selected).toBe(false);
3112
+ expect(options[1].selected).toBe(false);
3113
+ optionsSetSelected[0].calls.reset();
3114
+ optionsSetSelected[1].calls.reset();
3115
+
3116
+ // Support: Firefox 54+
3117
+ // Restore `originalSelectedDescriptor`
3118
+ if (addSpiesOnProto) {
3119
+ Object.defineProperty(
3120
+ optionProto,
3121
+ "selected",
3122
+ originalSelectedDescriptor,
3123
+ );
3124
+ }
3125
+ });
3126
+ }
3127
+
3128
+ if (window.MutationObserver) {
3129
+ // IE9 and IE10 do not support MutationObserver
3130
+ // Since the feature is only needed for a test, it's okay to skip these browsers
3131
+ it("should render the initial options only one time", () => {
3132
+ scope.value = ["black"];
3133
+ scope.values = ["black", "white", "red"];
3134
+ // observe-child-list adds a MutationObserver that we will read out after ngOptions
3135
+ // has been compiled
3136
+ createSelect({
3137
+ "ng-model": "selected",
3138
+ "ng-options": "value.name for value in values",
3139
+ multiple: "true",
3140
+ "observe-child-list": "",
3141
+ });
3142
+
3143
+ const optionEls = element[0].querySelectorAll("option");
3144
+ const records = childListMutationObserver.takeRecords();
3145
+
3146
+ expect(records.length).toBe(1);
3147
+ expect(records[0].addedNodes).toEqual(optionEls);
3148
+ });
3149
+ }
3150
+ });
3151
+
3152
+ describe("required state", () => {
3153
+ it("should set the error if the empty option is selected", () => {
3154
+ createSelect(
3155
+ {
3156
+ "ng-model": "selection",
3157
+ "ng-options": "item for item in values",
3158
+ required: "",
3159
+ },
3160
+ true,
3161
+ );
3162
+
3163
+ scope.$apply(() => {
3164
+ scope.values = ["a", "b"];
3165
+ scope.selection = scope.values[0];
3166
+ });
3167
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3168
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3169
+
3170
+ const options = element.find("option");
3171
+
3172
+ // // view -> model
3173
+ setSelectValue(element, 0);
3174
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3175
+ expect(ngModelCtrl.$error.required).toBeTruthy();
3176
+
3177
+ setSelectValue(element, 1);
3178
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3179
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3180
+
3181
+ // // model -> view
3182
+ scope.$apply("selection = null");
3183
+ expect(options[0].selected).toBe(true);
3184
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3185
+ expect(ngModelCtrl.$error.required).toBeTruthy();
3186
+ });
3187
+
3188
+ it("should validate with empty option and bound ngRequired", () => {
3189
+ createSelect(
3190
+ {
3191
+ "ng-model": "value",
3192
+ "ng-options": "item.name for item in values",
3193
+ "ng-required": "required",
3194
+ },
3195
+ true,
3196
+ );
3197
+
3198
+ scope.$apply(() => {
3199
+ scope.values = [
3200
+ { name: "A", id: 1 },
3201
+ { name: "B", id: 2 },
3202
+ ];
3203
+ scope.required = false;
3204
+ });
3205
+
3206
+ const options = element.find("option");
3207
+
3208
+ setSelectValue(element, 0);
3209
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3210
+
3211
+ scope.$apply("required = true");
3212
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3213
+
3214
+ scope.$apply("value = values[0]");
3215
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3216
+
3217
+ setSelectValue(element, 0);
3218
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3219
+
3220
+ scope.$apply("required = false");
3221
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3222
+ });
3223
+
3224
+ it("should treat an empty array as invalid when `multiple` attribute used", () => {
3225
+ createSelect(
3226
+ {
3227
+ "ng-model": "value",
3228
+ "ng-options": "item.name for item in values",
3229
+ "ng-required": "required",
3230
+ multiple: "",
3231
+ },
3232
+ true,
3233
+ );
3234
+
3235
+ scope.$apply(() => {
3236
+ scope.value = [];
3237
+ scope.values = [
3238
+ { name: "A", id: 1 },
3239
+ { name: "B", id: 2 },
3240
+ ];
3241
+ scope.required = true;
3242
+ });
3243
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3244
+
3245
+ scope.$apply(() => {
3246
+ // ngModelWatch does not set objectEquality flag
3247
+ // array must be replaced in order to trigger $formatters
3248
+ scope.value = [scope.values[0]];
3249
+ });
3250
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3251
+ });
3252
+
3253
+ it("should NOT set the error if the empty option is present but required attribute is not", () => {
3254
+ scope.$apply(() => {
3255
+ scope.values = ["a", "b"];
3256
+ });
3257
+
3258
+ createSingleSelect();
3259
+
3260
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3261
+ expect(element[0].classList.contains("ng-pristine")).toBeTrue();
3262
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3263
+ });
3264
+
3265
+ it("should NOT set the error if the unknown option is selected", () => {
3266
+ createSelect({
3267
+ "ng-model": "selection",
3268
+ "ng-options": "item for item in values",
3269
+ required: "",
3270
+ });
3271
+
3272
+ scope.$apply(() => {
3273
+ scope.values = ["a", "b"];
3274
+ scope.selection = "a";
3275
+ });
3276
+
3277
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3278
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3279
+
3280
+ scope.$apply('selection = "c"');
3281
+ expect(element[0].value).toBe("?");
3282
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3283
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3284
+ });
3285
+
3286
+ it("should allow falsy values as values", () => {
3287
+ createSelect(
3288
+ {
3289
+ "ng-model": "value",
3290
+ "ng-options": "item.value as item.name for item in values",
3291
+ "ng-required": "required",
3292
+ },
3293
+ true,
3294
+ );
3295
+
3296
+ scope.$apply(() => {
3297
+ scope.values = [
3298
+ { name: "True", value: true },
3299
+ { name: "False", value: false },
3300
+ ];
3301
+ scope.required = false;
3302
+ });
3303
+
3304
+ setSelectValue(element, 2);
3305
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3306
+ expect(scope.value).toBe(false);
3307
+
3308
+ scope.$apply("required = true");
3309
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3310
+ expect(scope.value).toBe(false);
3311
+ });
3312
+
3313
+ it("should validate after option list was updated", () => {
3314
+ createSelect(
3315
+ {
3316
+ "ng-model": "selection",
3317
+ "ng-options": "item for item in values",
3318
+ required: "",
3319
+ },
3320
+ true,
3321
+ );
3322
+
3323
+ scope.$apply(() => {
3324
+ scope.values = ["A", "B"];
3325
+ scope.selection = scope.values[0];
3326
+ });
3327
+
3328
+ expect(element[0].value).toBe("string:A");
3329
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3330
+ expect(ngModelCtrl.$error.required).toBeFalsy();
3331
+
3332
+ scope.$apply(() => {
3333
+ scope.values = ["C", "D"];
3334
+ });
3335
+
3336
+ expect(element[0].value).toBe("");
3337
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3338
+ expect(ngModelCtrl.$error.required).toBeTruthy();
3339
+ // ngModel sets undefined for invalid values
3340
+ expect(scope.selection).toBeUndefined();
3341
+ });
3342
+ });
3343
+
3344
+ describe("required and empty option", () => {
3345
+ it("should select the empty option after compilation", () => {
3346
+ createSelect(
3347
+ {
3348
+ name: "select",
3349
+ "ng-model": "value",
3350
+ "ng-options": "item for item in ['first', 'second', 'third']",
3351
+ required: "required",
3352
+ },
3353
+ true,
3354
+ );
3355
+
3356
+ expect(element.val()).toBe("");
3357
+ const emptyOption = element.find("option").eq(0);
3358
+ expect(emptyOption.prop("selected")).toBe(true);
3359
+ expect(emptyOption.val()).toBe("");
3360
+ });
3361
+ });
3362
+
3363
+ describe("ngModelCtrl", () => {
3364
+ it('should prefix the model value with the word "the" using $parsers', () => {
3365
+ createSelect({
3366
+ name: "select",
3367
+ "ng-model": "value",
3368
+ "ng-options": "item for item in ['first', 'second', 'third', 'fourth']",
3369
+ });
3370
+
3371
+ scope.form.select.$parsers.push((value) => `the ${value}`);
3372
+
3373
+ setSelectValue(element, 3);
3374
+ expect(scope.value).toBe("the third");
3375
+ expect(element).toEqualSelectValue("third");
3376
+ });
3377
+
3378
+ it('should prefix the view value with the word "the" using $formatters', () => {
3379
+ createSelect({
3380
+ name: "select",
3381
+ "ng-model": "value",
3382
+ "ng-options":
3383
+ "item for item in ['the first', 'the second', 'the third', 'the fourth']",
3384
+ });
3385
+
3386
+ scope.form.select.$formatters.push((value) => `the ${value}`);
3387
+
3388
+ scope.$apply(() => {
3389
+ scope.value = "third";
3390
+ });
3391
+ expect(element).toEqualSelectValue("the third");
3392
+ });
3393
+
3394
+ it("should fail validation when $validators fail", () => {
3395
+ createSelect({
3396
+ name: "select",
3397
+ "ng-model": "value",
3398
+ "ng-options": "item for item in ['first', 'second', 'third', 'fourth']",
3399
+ });
3400
+
3401
+ scope.form.select.$validators.fail = function () {
3402
+ return false;
3403
+ };
3404
+
3405
+ setSelectValue(element, 3);
3406
+ expect(element[0].classList.contains("ng-invalid")).toBeTrue();
3407
+ expect(scope.value).toBeUndefined();
3408
+ expect(element).toEqualSelectValue("third");
3409
+ });
3410
+
3411
+ it("should pass validation when $validators pass", () => {
3412
+ createSelect({
3413
+ name: "select",
3414
+ "ng-model": "value",
3415
+ "ng-options": "item for item in ['first', 'second', 'third', 'fourth']",
3416
+ });
3417
+
3418
+ scope.form.select.$validators.pass = function () {
3419
+ return true;
3420
+ };
3421
+
3422
+ setSelectValue(element, 3);
3423
+ expect(element[0].classList.contains("ng-valid")).toBeTrue();
3424
+ expect(scope.value).toBe("third");
3425
+ expect(element).toEqualSelectValue("third");
3426
+ });
3427
+
3428
+ it("should fail validation when $asyncValidators fail", () => {
3429
+ let $q = injector.get("$q");
3430
+ let defer;
3431
+ createSelect({
3432
+ name: "select",
3433
+ "ng-model": "value",
3434
+ "ng-options": "item for item in ['first', 'second', 'third', 'fourth']",
3435
+ });
3436
+
3437
+ scope.form.select.$asyncValidators.async = function () {
3438
+ defer = $q.defer();
3439
+ return defer.promise;
3440
+ };
3441
+
3442
+ setSelectValue(element, 3);
3443
+ expect(scope.form.select.$pending).toBeDefined();
3444
+ expect(scope.value).toBeUndefined();
3445
+ expect(element).toEqualSelectValue("third");
3446
+
3447
+ defer.reject();
3448
+ scope.$digest();
3449
+ expect(scope.form.select.$pending).toBeUndefined();
3450
+ expect(scope.value).toBeUndefined();
3451
+ expect(element).toEqualSelectValue("third");
3452
+ });
3453
+
3454
+ it("should pass validation when $asyncValidators pass", () => {
3455
+ let $q = injector.get("$q");
3456
+ let defer;
3457
+ createSelect({
3458
+ name: "select",
3459
+ "ng-model": "value",
3460
+ "ng-options": "item for item in ['first', 'second', 'third', 'fourth']",
3461
+ });
3462
+
3463
+ scope.form.select.$asyncValidators.async = function () {
3464
+ defer = $q.defer();
3465
+ return defer.promise;
3466
+ };
3467
+
3468
+ setSelectValue(element, 3);
3469
+ expect(scope.form.select.$pending).toBeDefined();
3470
+ expect(scope.value).toBeUndefined();
3471
+ expect(element).toEqualSelectValue("third");
3472
+
3473
+ defer.resolve();
3474
+ scope.$digest();
3475
+ expect(scope.form.select.$pending).toBeUndefined();
3476
+ expect(scope.value).toBe("third");
3477
+ expect(element).toEqualSelectValue("third");
3478
+ });
3479
+
3480
+ it("should not set $dirty with select-multiple after compilation", () => {
3481
+ scope.values = ["a", "b"];
3482
+ scope.selected = ["b"];
3483
+
3484
+ createSelect({
3485
+ "ng-model": "selected",
3486
+ multiple: true,
3487
+ "ng-options": "value for value in values",
3488
+ name: "select",
3489
+ });
3490
+
3491
+ expect(element.find("option")[1].selected).toBe(true);
3492
+ expect(scope.form.select.$pristine).toBe(true);
3493
+ });
3494
+ });
3495
+
3496
+ describe("selectCtrl api", () => {
3497
+ it("should reflect the status of empty and unknown option", () => {
3498
+ createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
3499
+
3500
+ const selectCtrl = element.controller("select");
3501
+
3502
+ scope.$apply(() => {
3503
+ scope.values = [{ name: "A" }, { name: "B" }];
3504
+ scope.isBlank = true;
3505
+ });
3506
+
3507
+ expect(element[0].value).toBe("");
3508
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3509
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3510
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3511
+
3512
+ // empty -> selection
3513
+ scope.$apply(() => {
3514
+ scope.selected = scope.values[0];
3515
+ });
3516
+
3517
+ expect(element[0].value).not.toBe("");
3518
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3519
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3520
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3521
+
3522
+ // remove empty
3523
+ scope.$apply("isBlank = false");
3524
+
3525
+ expect(element[0].value).not.toBe("");
3526
+ expect(selectCtrl.$hasEmptyOption()).toBe(false);
3527
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3528
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3529
+
3530
+ // selection -> unknown
3531
+ scope.$apply('selected = "unmatched"');
3532
+
3533
+ expect(element[0].value).toBe("?");
3534
+ expect(selectCtrl.$hasEmptyOption()).toBe(false);
3535
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3536
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3537
+
3538
+ // add empty
3539
+ scope.$apply("isBlank = true");
3540
+
3541
+ expect(element[0].value).toBe("?");
3542
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3543
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3544
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3545
+
3546
+ // unknown -> empty
3547
+ scope.$apply(() => {
3548
+ scope.selected = null;
3549
+ });
3550
+
3551
+ expect(element[0].value).toBe("");
3552
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3553
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3554
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3555
+
3556
+ // empty -> unknown
3557
+ scope.$apply('selected = "unmatched"');
3558
+
3559
+ expect(element[0].value).toBe("?");
3560
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3561
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3562
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(true);
3563
+
3564
+ // unknown -> selection
3565
+ scope.$apply(() => {
3566
+ scope.selected = scope.values[1];
3567
+ });
3568
+
3569
+ expect(element[0].value).not.toBe("");
3570
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3571
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(false);
3572
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3573
+
3574
+ // selection -> empty
3575
+ scope.$apply("selected = null");
3576
+
3577
+ expect(element[0].value).toBe("");
3578
+ expect(selectCtrl.$hasEmptyOption()).toBe(true);
3579
+ expect(selectCtrl.$isEmptyOptionSelected()).toBe(true);
3580
+ expect(selectCtrl.$isUnknownOptionSelected()).toBe(false);
3581
+ });
3582
+ });
3583
+ });