mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
ajax mixin 100% coverage, ready for production
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<style>
|
<style>
|
||||||
.v-select.dropdown {
|
.v-select {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,12 @@
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: all 150ms cubic-bezier(1.000, -0.115, 0.975, 0.855);
|
transition: all 150ms cubic-bezier(1.000, -0.115, 0.975, 0.855);
|
||||||
transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855);
|
transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-select.loading .open-indicator {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-select .open-indicator:before {
|
.v-select .open-indicator:before {
|
||||||
@@ -116,6 +122,47 @@
|
|||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-select .spinner {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 5px;
|
||||||
|
text-indent: -9999em;
|
||||||
|
border-top: .9em solid rgba(100,100,100,.1);
|
||||||
|
border-right: .9em solid rgba(100,100,100,.1);
|
||||||
|
border-bottom: .9em solid rgba(100,100,100,.1);
|
||||||
|
border-left: .9em solid rgba(60,60,60,.45);
|
||||||
|
transform: translateZ(0);
|
||||||
|
animation: vSelectSpinner 1.1s infinite linear;
|
||||||
|
transition: opacity .1s;
|
||||||
|
}
|
||||||
|
.v-select.loading .spinner {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.v-select .spinner,
|
||||||
|
.v-select .spinner:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes vSelectSpinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes vSelectSpinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -134,8 +181,9 @@
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
v-el:search
|
v-el:search
|
||||||
v-show="searchable"
|
:debounce="debounce"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
|
v-show="searchable"
|
||||||
@keydown.delete="maybeDeleteValue"
|
@keydown.delete="maybeDeleteValue"
|
||||||
@keyup.esc="onEscape"
|
@keyup.esc="onEscape"
|
||||||
@keydown.up.prevent="typeAheadUp"
|
@keydown.up.prevent="typeAheadUp"
|
||||||
@@ -150,6 +198,10 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<i v-el:open-indicator role="presentation" class="open-indicator"></i>
|
<i v-el:open-indicator role="presentation" class="open-indicator"></i>
|
||||||
|
|
||||||
|
<slot name="spinner">
|
||||||
|
<div class="spinner" v-show="onSearch && loading">Loading...</div>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-el:dropdown-menu v-show="open" :transition="transition" class="dropdown-menu" :style="{ 'max-height': maxHeight }">
|
<ul v-el:dropdown-menu v-show="open" :transition="transition" class="dropdown-menu" :style="{ 'max-height': maxHeight }">
|
||||||
@@ -169,9 +221,10 @@
|
|||||||
|
|
||||||
<script type="text/babel">
|
<script type="text/babel">
|
||||||
import pointerScroll from '../mixins/pointerScroll'
|
import pointerScroll from '../mixins/pointerScroll'
|
||||||
|
import ajax from '../mixins/ajax.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [pointerScroll],
|
mixins: [pointerScroll, ajax],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
@@ -536,8 +589,8 @@
|
|||||||
*/
|
*/
|
||||||
dropdownClasses() {
|
dropdownClasses() {
|
||||||
return {
|
return {
|
||||||
open: this.open,
|
searchable: this.searchable,
|
||||||
searchable: this.searchable
|
loading: this.loading
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
module.exports = {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Toggles the adding of a 'loading' class to the main
|
||||||
|
* .v-select wrapper. Useful to control UI state when
|
||||||
|
* results are being processed through AJAX.
|
||||||
|
*/
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a callback function that will be
|
||||||
|
* run when the search text changes.
|
||||||
|
*
|
||||||
|
* loading() accepts a boolean value, and can
|
||||||
|
* be used to toggle a loading class from
|
||||||
|
* the onSearch callback.
|
||||||
|
*
|
||||||
|
* @param {search} String Current search text
|
||||||
|
* @param {loading} Function(bool) Toggle loading class
|
||||||
|
*/
|
||||||
|
onSearch: {
|
||||||
|
type: Function,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds to wait before
|
||||||
|
* invoking this.onSearch(). Used to prevent
|
||||||
|
* sending an AJAX request until input is complete.
|
||||||
|
*/
|
||||||
|
debounce: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
/**
|
||||||
|
* If a callback & search text has been provided,
|
||||||
|
* invoke the onSearch callback.
|
||||||
|
*/
|
||||||
|
search() {
|
||||||
|
if (this.search.length > 0 && this.onSearch) {
|
||||||
|
this.onSearch(this.search, this.toggleLoading)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Toggle this.loading. Optionally pass a boolean
|
||||||
|
* value. If no value is provided, this.loading
|
||||||
|
* will be set to the opposite of it's current value.
|
||||||
|
* @param toggle Boolean
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
toggleLoading(toggle = null) {
|
||||||
|
if (toggle == null) {
|
||||||
|
return this.loading = !this.loading
|
||||||
|
}
|
||||||
|
return this.loading = toggle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import Vue from 'vue'
|
|||||||
import vSelect from 'src/components/Select.vue'
|
import vSelect from 'src/components/Select.vue'
|
||||||
import pointerScroll from 'src/mixins/pointerScroll.js'
|
import pointerScroll from 'src/mixins/pointerScroll.js'
|
||||||
|
|
||||||
|
Vue.component('v-select', vSelect)
|
||||||
|
|
||||||
// http://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
|
// http://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
|
||||||
const Mock = require('!!vue?inject!src/components/Select.vue')
|
const Mock = require('!!vue?inject!src/components/Select.vue')
|
||||||
|
|
||||||
@@ -726,4 +728,90 @@ describe('Select.vue', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Asynchronous Loading', () => {
|
||||||
|
it('can toggle the loading class', () => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select v-ref:select></v-select></div>',
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
vm.$refs.select.toggleLoading()
|
||||||
|
expect(vm.$refs.select.loading).toEqual(true)
|
||||||
|
|
||||||
|
vm.$refs.select.toggleLoading(true)
|
||||||
|
expect(vm.$refs.select.loading).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger the onSearch callback when the search text changes', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select v-ref:select :on-search="foo"></v-select></div>',
|
||||||
|
methods: {
|
||||||
|
foo() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
spyOn(vm.$refs.select, 'onSearch')
|
||||||
|
vm.$refs.select.search = 'foo'
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.onSearch).toHaveBeenCalledWith('foo', vm.$refs.select.toggleLoading)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not trigger the onSearch callback if the search text is empty', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select v-ref:select search="foo" :on-search="foo"></v-select></div>',
|
||||||
|
methods: {
|
||||||
|
foo() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
spyOn(vm.$refs.select, 'onSearch')
|
||||||
|
vm.$refs.select.search = ''
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.onSearch).not.toHaveBeenCalled()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can set loading to false from the onSearch callback', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select loading v-ref:select :on-search="foo"></v-select></div>',
|
||||||
|
methods: {
|
||||||
|
foo(search, loading) {
|
||||||
|
loading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
vm.$refs.select.search = 'foo'
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.loading).toEqual(false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can set loading to true from the onSearch callback', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: '<div><v-select loading v-ref:select :on-search="foo"></v-select></div>',
|
||||||
|
methods: {
|
||||||
|
foo(search, loading) {
|
||||||
|
loading(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).$mount()
|
||||||
|
|
||||||
|
let select = vm.$refs.select
|
||||||
|
select.onSearch(select.search, select.toggleLoading)
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.loading).toEqual(true)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user