Skip to content

Commit ebcdcc5

Browse files
doitssagalbot
authored andcommitted
Make sure selected value is an option after option changed and react to value property changes even if tracking value internally (sagalbot#914)
* make sure selected tracked value is an option if possible Before this case did not work correctly: - Select was rendered with *no* options, but *with* a saved value - Options were fetched by ajax and options prop was updated - Reduce function if passed What happens without this commit is that the selected tracked value simply was the raw reduced value (previously saved). Which means that displaying a label for example does not work if the label comes from the unreduced option. This commit makes sure that the internal tracked value is checked against all options not only once the select is created but additionally when options change. * remove useless keys - first key was always undefined - second key was always the index which is not usefull at all since it changes based on the order * add test for setting value after option changed * correctly react to value property changes if tracking internally fixes sagalbot#855, sagalbot#842 * add getOptionKey prop * yarn upgrade doc * add getOptionKey api doc and fix links * yarn upgrade * do not use key on slot * fix label spec
1 parent 8ef15a1 commit ebcdcc5

File tree

7 files changed

+3829
-4052
lines changed

7 files changed

+3829
-4052
lines changed

docs/api/props.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ getOptionLabel: {
178178
return console.warn(
179179
`[vue-select warn]: Label key "option.${this.label}" does not` +
180180
` exist in options object ${JSON.stringify(option)}.\n` +
181-
'http://sagalbot.github.io/vue-select/#ex-labels'
181+
'https://vue-select.org/api/props.html#getoptionlabel'
182182
)
183183
}
184184
return option[this.label]
@@ -188,6 +188,38 @@ getOptionLabel: {
188188
},
189189
```
190190

191+
## getOptionKey
192+
193+
Callback to get an option key. If `option`
194+
is an object and has an `id`, returns `option.id`
195+
by default, otherwise tries to serialize `option`
196+
to JSON.
197+
198+
The key must be unique for an option.
199+
200+
```js
201+
getOptionKey: {
202+
type: Function,
203+
default(option) {
204+
if (typeof option === 'object' && option.id) {
205+
return option.id
206+
} else {
207+
try {
208+
return JSON.stringify(option)
209+
} catch(e) {
210+
return console.warn(
211+
`[vue-select warn]: Could not stringify option ` +
212+
`to generate unique key. Please provide 'getOptionKey' prop ` +
213+
`to return a unique key for each option.\n` +
214+
'https://vue-select.org/api/props.html#getoptionkey'
215+
)
216+
return null
217+
}
218+
}
219+
}
220+
},
221+
```
222+
191223
## onTab
192224

193225
Select the current value if `selectOnTab` is enabled

docs/yarn.lock

Lines changed: 1511 additions & 1974 deletions
Large diffs are not rendered by default.

src/components/Select.vue

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
:deselect="deselect"
1414
:multiple="multiple"
1515
:disabled="disabled">
16-
<span class="vs__selected" v-bind:key="option.index">
16+
<span :key="getOptionKey(option)" class="vs__selected">
1717
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
1818
{{ getOptionLabel(option) }}
1919
</slot>
@@ -55,7 +55,7 @@
5555
<li
5656
role="option"
5757
v-for="(option, index) in filteredOptions"
58-
:key="index"
58+
:key="getOptionKey(option)"
5959
class="vs__dropdown-option"
6060
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer }"
6161
@mouseover="typeAheadPointer = index"
@@ -245,7 +245,7 @@
245245
return console.warn(
246246
`[vue-select warn]: Label key "option.${this.label}" does not` +
247247
` exist in options object ${JSON.stringify(option)}.\n` +
248-
'http://sagalbot.github.io/vue-select/#ex-labels'
248+
'https://vue-select.org/api/props.html#getoptionlabel'
249249
)
250250
}
251251
return option[this.label]
@@ -254,6 +254,39 @@
254254
}
255255
},
256256
257+
/**
258+
* Callback to get an option key. If {option}
259+
* is an object and has an {id}, returns {option.id}
260+
* by default, otherwise tries to serialize {option}
261+
* to JSON.
262+
*
263+
* The key must be unique for an option.
264+
*
265+
* @type {Function}
266+
* @param {Object || String} option
267+
* @return {String}
268+
*/
269+
getOptionKey: {
270+
type: Function,
271+
default(option) {
272+
if (typeof option === 'object' && option.id) {
273+
return option.id
274+
} else {
275+
try {
276+
return JSON.stringify(option)
277+
} catch(e) {
278+
return console.warn(
279+
`[vue-select warn]: Could not stringify option ` +
280+
`to generate unique key. Please provide'getOptionKey' prop ` +
281+
`to return a unique key for each option.\n` +
282+
'https://vue-select.org/api/props.html#getoptionkey'
283+
)
284+
return null
285+
}
286+
}
287+
}
288+
},
289+
257290
/**
258291
* Select the current value if selectOnTab is enabled
259292
*/
@@ -437,12 +470,28 @@
437470
/**
438471
* Maybe reset the value
439472
* when options change.
473+
* Make sure selected option
474+
* is correct.
440475
* @return {[type]} [description]
441476
*/
442477
options(val) {
443478
if (!this.taggable && this.resetOnOptionsChange) {
444479
this.clearSelection()
445480
}
481+
482+
if (this.value && this.isTrackingValues) {
483+
this.setInternalValueFromOptions(this.value)
484+
}
485+
},
486+
487+
/**
488+
* Make sure to update internal
489+
* value if prop changes outside
490+
*/
491+
value(val) {
492+
if (this.isTrackingValues) {
493+
this.setInternalValueFromOptions(val)
494+
}
446495
},
447496
448497
/**
@@ -453,24 +502,33 @@
453502
*/
454503
multiple() {
455504
this.clearSelection()
456-
},
505+
}
457506
},
458507
459508
created() {
460509
this.mutableLoading = this.loading;
461510
462-
if (this.$options.propsData.hasOwnProperty('reduce') && this.value) {
463-
if (Array.isArray(this.value)) {
464-
this.$data._value = this.value.map(value => this.findOptionFromReducedValue(value));
465-
} else {
466-
this.$data._value = this.findOptionFromReducedValue(this.value);
467-
}
511+
if (this.value && this.isTrackingValues) {
512+
this.setInternalValueFromOptions(this.value)
468513
}
469514
470515
this.$on('option:created', this.maybePushTag)
471516
},
472517
473518
methods: {
519+
/**
520+
* Make sure tracked value is
521+
* one option if possible.
522+
* @param {Object|String} value
523+
* @return {void}
524+
*/
525+
setInternalValueFromOptions(value) {
526+
if (Array.isArray(value)) {
527+
this.$data._value = value.map(val => this.findOptionFromReducedValue(val));
528+
} else {
529+
this.$data._value = this.findOptionFromReducedValue(value);
530+
}
531+
},
474532
475533
/**
476534
* Select a given option.

tests/unit/Labels.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("Labels", () => {
2121
Select.vm.open = true;
2222
expect(spy).toHaveBeenCalledWith(
2323
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
24-
"\nhttp://sagalbot.github.io/vue-select/#ex-labels"
24+
"\nhttps://vue-select.org/api/props.html#getoptionlabel"
2525
);
2626
});
2727

tests/unit/ReactiveOptions.spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ describe("Reset on options change", () => {
2323
Select.setProps({options: ["four", "five", "six"]});
2424
expect(Select.vm.selectedValue).toEqual([]);
2525
});
26+
27+
it("should return correct selected value when the options property changes and a new option matches", () => {
28+
const Select = shallowMount(VueSelect, {
29+
propsData: { value: "one", options: [], reduce(option) { return option.value } }
30+
});
31+
32+
Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
33+
expect(Select.vm.selectedValue).toEqual([{ label: "oneLabel", value: "one" }]);
34+
});
2635
});

tests/unit/Reduce.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,19 @@ describe("When reduce prop is defined", () => {
182182
});
183183

184184
});
185+
186+
it("reacts correctly when value propery changes", () => {
187+
const optionToChangeTo = { id: 1, label: "Foo" };
188+
const Select = shallowMount(VueSelect, {
189+
propsData: {
190+
value: 2,
191+
reduce: option => option.id,
192+
options: [optionToChangeTo, { id: 2, label: "Bar" }]
193+
}
194+
});
195+
196+
Select.setProps({ value: optionToChangeTo.id });
197+
198+
expect(Select.vm.selectedValue).toEqual([optionToChangeTo]);
199+
});
185200
});

0 commit comments

Comments
 (0)