Skip to content

Commit cb1b01d

Browse files
authored
Merge pull request #1824 from shentao/1763-rationale-behind-sorting-filtered-options
1763-rationale-behind-sorting-filtered-options
2 parents b60ab17 + 7c549af commit cb1b01d

File tree

5 files changed

+131
-30
lines changed

5 files changed

+131
-30
lines changed

documentation/src/components/Props.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@
190190
<td class="table__td">false</td>
191191
<td class="table__td">Disabled the search input focusing when the multiselect opens</td>
192192
</tr>
193+
<tr class="table__tr">
194+
<td class="table__td"><strong>filteringSortFunc</strong></td>
195+
<td class="table__td">Function => Int</td>
196+
<td class="table__td"></td>
197+
<td class="table__td">Allows a custom sorting function when the user searching.
198+
This function will be the <i>compareFn</i> argument for
199+
<i><a target="_blank"
200+
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">
201+
Array.sort()
202+
</a></i>
203+
function, so will require two arguments. <br/><b>Added in v3.2.0</b></td>
204+
</tr>
193205
<tr class="table__tr">
194206
<td class="table__td utils--center" colspan="4"><strong>Multiselect.vue</strong></td>
195207
</tr>

documentation/src/components/example-sections/SelectWithSearch.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
object properties look at the <a href="#sub-asynchronous-select">ajax search example</a>.</p>
77
<p><code>custom-label</code> accepts a function with the <code>option</code> object as the first param. It should
88
return a string which is then used to display a custom label.</p>
9+
<p>When sorting a filtered list, <code>filteringSortFunc</code> accepts a function for use in <code>Array.sort()</code>.
10+
By default, it orders by the ascending length of each option.</p>
911
<CodeDemoAndExample demo-name="SingleSelectSearch"/>
1012
</div>
1113
</template>

src/Multiselect.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ export default {
327327
type: Number,
328328
default: 0
329329
},
330+
/**
331+
* Adds Required attribute to the input element when there is no value selected
332+
* @default false
333+
* @type {Boolean}
334+
*/
330335
required: {
331336
type: Boolean,
332337
default: false

src/multiselectMixin.js

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ function includes (str, query) {
1717
return text.indexOf(query.trim()) !== -1
1818
}
1919

20-
function filterOptions (options, search, label, customLabel) {
21-
return search
22-
? options
23-
.filter((option) => includes(customLabel(option, label), search))
24-
.sort((a, b) => customLabel(a, label).length - customLabel(b, label).length)
25-
: options
26-
}
27-
2820
function stripGroups (options) {
2921
return options.filter((option) => !option.$isLabel)
3022
}
@@ -44,25 +36,6 @@ function flattenOptions (values, label) {
4436
}, [])
4537
}
4638

47-
function filterGroups (search, label, values, groupLabel, customLabel) {
48-
return (groups) =>
49-
groups.map((group) => {
50-
/* istanbul ignore else */
51-
if (!group[values]) {
52-
console.warn('Options passed to vue-multiselect do not contain groups, despite the config.')
53-
return []
54-
}
55-
const groupOptions = filterOptions(group[values], search, label, customLabel)
56-
57-
return groupOptions.length
58-
? {
59-
[groupLabel]: group[groupLabel],
60-
[values]: groupOptions
61-
}
62-
: []
63-
})
64-
}
65-
6639
const flow = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
6740

6841
export default {
@@ -314,10 +287,19 @@ export default {
314287
* Prevent autofocus
315288
* @default false
316289
* @type {Boolean}
317-
*/
290+
*/
318291
preventAutofocus: {
319292
type: Boolean,
320293
default: false
294+
},
295+
/**
296+
* Allows a custom function for sorting search/filtered results.
297+
* @default null
298+
* @type {Function}
299+
*/
300+
filteringSortFunc: {
301+
type: Function,
302+
default: null
321303
}
322304
},
323305
mounted () {
@@ -349,7 +331,7 @@ export default {
349331
if (this.internalSearch) {
350332
options = this.groupValues
351333
? this.filterAndFlat(options, normalizedSearch, this.label)
352-
: filterOptions(options, normalizedSearch, this.label, this.customLabel)
334+
: this.filterOptions(options, normalizedSearch, this.label, this.customLabel)
353335
} else {
354336
options = this.groupValues ? flattenOptions(this.groupValues, this.groupLabel)(options) : options
355337
}
@@ -423,7 +405,7 @@ export default {
423405
*/
424406
filterAndFlat (options, search, label) {
425407
return flow(
426-
filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
408+
this.filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
427409
flattenOptions(this.groupValues, this.groupLabel)
428410
)(options)
429411
},
@@ -723,6 +705,51 @@ export default {
723705
this.preferredOpenDirection = 'above'
724706
this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight)
725707
}
708+
},
709+
/**
710+
* Filters and sorts the options ready for selection
711+
* @param {Array} options
712+
* @param {String} search
713+
* @param {String} label
714+
* @param {Function} customLabel
715+
* @returns {Array}
716+
*/
717+
filterOptions (options, search, label, customLabel) {
718+
return search
719+
? options
720+
.filter((option) => includes(customLabel(option, label), search))
721+
.sort((a, b) => {
722+
if (typeof this.filteringSortFunc === 'function') {
723+
return this.filteringSortFunc(a, b)
724+
}
725+
return customLabel(a, label).length - customLabel(b, label).length
726+
})
727+
: options
728+
},
729+
/**
730+
*
731+
* @param {String} search
732+
* @param {String} label
733+
* @param {String} values
734+
* @param {String} groupLabel
735+
* @param {function} customLabel
736+
* @returns {function(*): *}
737+
*/
738+
filterGroups (search, label, values, groupLabel, customLabel) {
739+
return (groups) => groups.map((group) => {
740+
/* istanbul ignore else */
741+
if (!group[values]) {
742+
console.warn('Options passed to vue-multiselect do not contain groups, despite the config.')
743+
return []
744+
}
745+
const groupOptions = this.filterOptions(group[values], search, label, customLabel)
746+
747+
return groupOptions.length
748+
? {
749+
[groupLabel]: group[groupLabel], [values]: groupOptions
750+
}
751+
: []
752+
})
726753
}
727754
}
728755
}

tests/unit/Multiselect.spec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,4 +1824,59 @@ describe('Multiselect.vue', () => {
18241824
expect(wrapper.get('.multiselect__input').attributes('required')).toBeUndefined()
18251825
})
18261826
})
1827+
describe('filteringSortFunc prop', () => {
1828+
const options = [
1829+
{ name: 'Vue.js', language: 'JavaScript' },
1830+
{ name: 'Laravel', language: 'PHP' },
1831+
{ name: 'Rails', language: 'Ruby' },
1832+
{ name: 'Sinatra', language: 'Ruby' },
1833+
{ name: 'Phoenix', language: 'Elixir' }
1834+
]
1835+
1836+
const customLabel = ({ name, language }) => {
1837+
return `${name} — [${language}]`
1838+
}
1839+
1840+
test('should use default sorting when no function is provided', async () => {
1841+
const wrapper = shallowMount(Multiselect, {
1842+
props: {
1843+
modelValue: [],
1844+
options,
1845+
customLabel,
1846+
label: 'name',
1847+
trackBy: 'name'
1848+
},
1849+
data () {
1850+
return {
1851+
search: 'a'
1852+
}
1853+
}
1854+
})
1855+
1856+
expect(wrapper.vm.filteredOptions[0].name).toBe('Rails')
1857+
expect(wrapper.vm.filteredOptions[3].name).toBe('Vue.js')
1858+
})
1859+
test('should use custom sorting when function is provided', async () => {
1860+
const wrapper = shallowMount(Multiselect, {
1861+
props: {
1862+
modelValue: [],
1863+
options,
1864+
customLabel,
1865+
label: 'name',
1866+
trackBy: 'name',
1867+
filteringSortFunc: (a, b) => {
1868+
return a.name.length - b.name.length
1869+
}
1870+
},
1871+
data () {
1872+
return {
1873+
search: 'a'
1874+
}
1875+
}
1876+
})
1877+
expect(wrapper.vm.filteredOptions[0].name).toBe('Rails')
1878+
expect(wrapper.vm.filteredOptions[1].name).toBe('Vue.js')
1879+
expect(wrapper.vm.filteredOptions[2].name).toBe('Laravel')
1880+
})
1881+
})
18271882
})

0 commit comments

Comments
 (0)