2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-05-23 03:54:04 +03:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
es
2017-03-21 15:23:05 +08:00
39 changed files with 1492 additions and 1095 deletions
+2 -1
View File
@@ -3,4 +3,5 @@ node_modules
npm-debug.log
.idea
test/unit/coverage
.coveralls.yml
.coveralls.yml
.flowconfig
+1 -5
View File
@@ -2,9 +2,5 @@ language: node_js
node_js:
- "5"
- "5.1"
- "4"
- "4.2"
- "4.1"
- "4.0"
after_success:
- codeclimate-test-reporter < ./test/unit/coverage/lcov.info
- codeclimate-test-reporter < ./test/unit/coverage/lcov.info
+64 -190
View File
@@ -1,229 +1,103 @@
# vue-select [![Build Status](https://travis-ci.org/sagalbot/vue-select.svg?branch=master)](https://travis-ci.org/sagalbot/vue-select) [![Code Coverage](https://img.shields.io/codeclimate/coverage/github/sagalbot/vue-select.svg?style=flat-square)](https://codeclimate.com/github/sagalbot/vue-select) [![No Dependencies](https://img.shields.io/gemnasium/sagalbot/vue-select.svg?style=flat-square)](https://gemnasium.com/github.com/sagalbot/vue-select) ![MIT License](https://img.shields.io/github/license/sagalbot/vue-select.svg?style=flat-square) ![Current Release](https://img.shields.io/github/release/sagalbot/vue-select.svg?style=flat-square)
> A native Vue.js component that provides similar functionality to Select2 without the overhead of jQuery.
Rather than bringing in jQuery just to use Select2 or Chosen, this Vue.js component provides similar functionality without the extra overhead of jQuery, while providing the same awesome data-binding features you expect from Vue. Vue-select has no JavaScript dependencies other than Vue, and is designed to mimic [Select2](https://github.com/select2/select2).
> A native Vue.js select component that provides similar functionality to Select2 without the overhead of jQuery.
#### Features
- **AJAX Support +v1.2.0**
- Tagging Support **+v.1.1.0**
- No JS Dependencies
- AJAX Support
- Tagging
- List Filtering/Searching
- Supports Vuex
- Select Single/Multiple Options
- Bootstrap Friendly Markup
- +95% Test Coverage
- ~32kb minified
#### Upcoming/In Progress
## Documentation
- **[Demo & Docs](http://sagalbot.github.io/vue-select/)**
- **[Example on JSBin](http://jsbin.com/saxaru/8/edit?html,js,output)**
- ~~Tagging (adding options not present in list, see `taggable` branch)~~ **+v.1.1.0**
- ~~Asyncronous Option Loading~~ **+v.1.2.0**
- Rich Option Templating
## Install
## Live Examples & Docs
- [Demo & Docs](http://sagalbot.github.io/vue-select/)
- [Live Example on JSBin](http://jsbin.com/saxaru/5/edit?html,js,output)
###### Vue Compatibility
- `vue ~2.0` use `vue-select ~2.0`
- `vue ~1.0` use `vue-select ~1.0`
## Install / Usage
#### NPM
Install the package. _You should install `vue-select@1.3.3` for use with vue `~1.0`._
#### NPM Based WorkFlows
``` bash
```bash
$ npm install vue-select
```
```html
<template>
<div id="myApp">
<v-select :value.sync="selected" :options="options"></v-select>
</div>
</template>
Register the component
<script>
```js
import Vue from 'vue'
import vSelect from 'vue-select'
export default {
components: {vSelect},
data() {
return {
selected: null,
options: ['foo','bar','baz']
}
}
}
</script>
Vue.component(vSelect)
```
#### Browser Globals
`v1.3.0+` no longer requires any toolchain to use the component:
Just include `vue` & `vue-select.js` - I recommend using [npmcdn](https://npmcdn.com/#/).
You may now use the component in your markup
```html
<!-- use the latest release -->
<script src="https://npmcdn.com/vue-select@latest"></script>
<!-- or point to a specific release -->
<script src="https://npmcdn.com/vue-select@1.30"></script>
<v-select v-model="selected" :options="['foo','bar']"></v-select>
```
#### CDN
Just include `vue` & `vue-select.js` - I recommend using [unpkg](https://unpkg.com/#/).
```html
<script scr="https://unpkg.com/vue@latest"></script>
<!-- use the latest release -->
<script src="https://unpkg.com/vue-select@latest"></script>
<!-- or point to a specific release -->
<script src="https://unpkg.com/vue-select@1.3.3"></script>
```
Then register the component in your javascript:
```js
Vue.component('v-select', VueSelect.VueSelect);
```
From there you can use as normal. Here's an [example on JSBin](http://jsbin.com/saxaru/5/edit?html,js,output).
You may now use the component in your markup
## Parameters
```javascript
/**
* Contains the currently selected value. Very similar to a
* `value` attribute on an <input>. In most cases, you'll want
* to set this as a two-way binding, using :value.sync. However,
* this will not work with Vuex, in which case you'll need to use
* the onChange callback property.
* @type {Object||String||null}
*/
value: {
default: null
},
/**
* An array of strings or objects to be used as dropdown choices.
* If you are using an array of objects, vue-select will look for
* a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A
* custom label key can be set with the `label` prop.
* @type {Object}
*/
options: {
type: Array,
default() { return [] },
},
/**
* Sets the max-height property on the dropdown list.
* @deprecated
* @type {String}
*/
maxHeight: {
type: String,
default: '400px'
},
/**
* Enable/disable filtering the options.
* @type {Boolean}
*/
searchable: {
type: Boolean,
default: true
},
/**
* Equivalent to the `multiple` attribute on a `<select>` input.
* @type {Object}
*/
multiple: {
type: Boolean,
default: false
},
/**
* Equivalent to the `placeholder` attribute on an `<input>`.
* @type {Object}
*/
placeholder: {
type: String,
default: ''
},
/**
* Sets a Vue transition property on the `.dropdown-menu`. vue-select
* does not include CSS for transitions, you'll need to add them yourself.
* @type {String}
*/
transition: {
type: String,
default: 'expand'
},
/**
* Enables/disables clearing the search text when an option is selected.
* @type {Boolean}
*/
clearSearchOnSelect: {
type: Boolean,
default: true
},
/**
* Tells vue-select what key to use when generating option
* labels when each `option` is an object.
* @type {String}
*/
label: {
type: String,
default: 'label'
},
/**
* An optional callback function that is called each time the selected
* value(s) change. When integrating with Vuex, use this callback to trigger
* an action, rather than using :value.sync to retreive the selected value.
* @type {Function}
* @default {null}
*/
onChange: Function,
/**
* Enable/disable creating options from searchInput.
* @type {Boolean}
*/
taggable: {
type: Boolean,
default: false
},
/**
* When true, newly created tags will be added to
* the options list.
* @type {Boolean}
*/
pushTags: {
type: Boolean,
default: false
},
/**
* User defined function for adding Options
* @type {Function}
*/
createOption: {
type: Function,
default: function (newOption) {
if (typeof this.options[0] === 'object') {
return {[this.label]: newOption}
}
return newOption
}
}
```html
<v-select v-model="selected" :options="['foo','bar']"></v-select>
```
Here's an [example on JSBin](http://jsbin.com/saxaru/5/edit?html,js,output).
## Build Setup for Contributing
## Basic Usage
If there's a feature you'd like to see or you find a bug, feel free to fork and submit a PR. If your adding functionality, add tests to go with it.
#### Syncing a Selected Value
``` bash
# install dependencies
npm install
The most common use case for `vue-select` is to have the chosen value synced with a parent component. `vue-select` takes advantage of the `v-model` syntax to sync values with a parent.
# serve with hot reload at localhost:8080
npm run dev
```html
<v-select v-model="selected"></v-select>
```
```js
new Vue({
data: {
selected: null
}
})
```
# run unit tests
npm test
#### Setting Options
# run unit tests on save
npm run test-watch
```
`vue-select` accepts arrays of strings and objects to use as options through the `options` prop.
```html
<v-select :options="['foo','bar']"></v-select>
```
When provided an array of objects, `vue-select` will display a single value of the object. By default, `vue-select` will look for a key named 'label' on the object to use as display text.
```html
<v-select :options="[{label: 'foo', value: 'Foo'}]"></v-select>
```
### For more information, please visit the [vue-select documentation.](https://sagalbot.github.io/vue-select)
+8 -6
View File
@@ -5,7 +5,7 @@ var projectRoot = path.resolve(__dirname, '../')
module.exports = {
entry: {
app: './src/dev.js'
app: process.argv.indexOf('--docs') > 0 ? './docs/docs.js' : './src/dev.js',
},
output: {
path: config.build.assetsRoot,
@@ -19,7 +19,9 @@ module.exports = {
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../docs/assets'),
'mixins': path.resolve(__dirname, '../src/mixins'),
'components': path.resolve(__dirname, '../docs/components')
'components': path.resolve(__dirname, '../src/components'),
'docs': path.resolve(__dirname, '../docs'),
'vue$': 'vue/dist/vue.common.js',
}
},
resolveLoader: {
@@ -29,21 +31,21 @@ module.exports = {
loaders: [
{
test: /\.vue$/,
loader: 'vue'
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel',
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.json$/,
loader: 'json'
loader: 'json-loader'
},
{
test: /\.html$/,
loader: 'vue-html'
loader: 'vue-html-loader'
},
{
test: /\.(png|jpe?g|gif)(\?.*)?$/,
+8 -3
View File
@@ -12,7 +12,7 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) {
module.exports = merge(baseWebpackConfig, {
module: {
loaders: utils.styleLoaders()
loaders: utils.styleLoaders().concat({ test: /\.md$/, loader: "html!markdown" })
},
// eval-source-map is faster for development
devtool: '#eval-source-map',
@@ -27,8 +27,13 @@ module.exports = merge(baseWebpackConfig, {
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
template: process.argv.indexOf('--docs') > 0 ? './docs/docs.html' : 'dev.html',
inject: true
})
]
],
markdownLoader: {
highlight: function (code) {
return require('highlight.js').highlightAuto(code).value;
}
}
})
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue Select Dev</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"> -->
<style>
html,
body,
#app {
height: 100vh;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.v-select {
width: 25em;
margin: 1em;
}
</style>
</head>
<body>
<div id="app">
<v-select placeholder="multiple" multiple :options="options"></v-select>
<v-select placeholder="multiple, taggable" multiple taggable :options="options" no-drop></v-select>
<v-select placeholder="multiple, taggable, push-tags" multiple push-tags taggable :options="[{label: 'Foo', value: 'foo'}]"></v-select>
</div>
</body>
</html>
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+30 -8
View File
@@ -6,10 +6,22 @@
</style>
<template>
<div class="container">
<div class="col-md-10 col-md-offset-1">
<examples></examples>
<params></params>
<div id="docs" class="container-fluid">
<div class="col-md-2 col-md-offset-1">
<ul class="nav nav-pills nav-stacked">
<li><a href="#">Install &amp; Usage</a></li>
<li><a href="#">Examples</a></li>
<li><a href="#">Ajax</a></li>
<li><a href="#">Parameters</a></li>
</ul>
</div>
<div class="col-md-7">
<article v-html="install"></article>
<article v-html="vModel"></article>
<article v-html="single"></article>
<article v-html="reactive"></article>
<article v-html="labels"></article>
<article v-html="ajax"></article>
</div>
</div>
</template>
@@ -22,11 +34,21 @@
* for the demo site at http://sagalbot.github.io/vue-select/.
*/
import Examples from './components/Examples.vue'
import Params from './components/Params.vue'
import Ajax from './components/snippets/Ajax.vue'
// import Examples from './components/Examples.vue'
// import Params from './components/Params.vue'
// import Ajax from './components/snippets/Ajax.vue'
export default {
components: { Params, Examples, Ajax }
// components: { Params, Examples, Ajax }
data () {
return {
install: require('./md/Install.md'),
vModel: require('./md/VModel.md'),
single: require('./md/SingleMultiple.md'),
reactive: require('./md/ReactiveOptions.md'),
labels: require('./md/CustomLabels.md'),
ajax: require('./md/Ajax.md'),
}
},
}
</script>
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -25,8 +25,10 @@ pre[class*="language-"] {
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
background: #f5f7ff;
background: $code-white;
color: #5e6687;
border-radius: 0;
border: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
@@ -150,4 +152,4 @@ pre[class*="language-"] {
pre > code.highlight {
outline: 0.4em solid #c94922;
outline-offset: .4em;
}
}
+5 -5
View File
@@ -1,15 +1,15 @@
$orange: #F16745;
$orange: #e96900;
$yellow: #FFC65D;
$green: #7BC8A4;
$green: #42b983;
$blue: #4CC3D9;
$purple: #93648D;
$black: #404040;
$black: #34495e;
$red: #ff6666;
// Code
$code-blue: #66d9ef;
$code-purple: #ae81ff;
$code-black: #272822;
$code-white: #f8f8f2;
$code-white: #f8f8f8;
$code-grey: #708090;
$code-green: #a6e22e;
+3 -3
View File
@@ -1,5 +1,5 @@
<template>
<code :class="class"><slot></slot></code>
<code :class="cssClass"><slot></slot></code>
</template>
<script type="text/babel">
@@ -12,9 +12,9 @@
export default {
props: ['lang'],
computed: {
class () {
cssClass () {
return `language-${this.lang}`
}
}
}
</script>
</script>
+9 -8
View File
@@ -7,8 +7,8 @@
</div>
<div class="col-md-5">
<p>The resulting vue-select, and it's value: <v-code lang="json">{{ install | json }}</v-code></p>
<v-select :value.sync="install" :options="['foo','bar','baz']"></v-select>
<p>The resulting vue-select, and it's value: <v-code lang="json">{{ install }}</v-code></p>
<v-select v-model="install" :options="['foo','bar','baz']"></v-select>
</div>
</div>
@@ -65,7 +65,7 @@
<p>The <code>.sync</code> data-binding modifier is completely optional. You may use <code>value</code> without a two-way binding to preselect options.</p>
<p>Here we have preselected 'Canada' by setting <code>syncedVal: 'Canada'</code> on the parent component. The buttons below demonstrate how you can set the <code>value</code> from the parent.</p>
<p>Current value: <v-code>{{ syncedVal | json }}</v-code></p>
<p>Current value: <v-code>{{ syncedVal }}</v-code></p>
</div>
<div class="col-md-6">
@@ -73,7 +73,7 @@
<pre><v-code lang="markup">&#x3C;v-select :value.sync=&#x22;syncedVal&#x22; :options=&#x22;countries&#x22;&#x3E;&#x3C;/v-select&#x3E;</v-code></pre>
</div>
<div class="form-group">
<v-select :options="simple" :value.sync="syncedVal"></v-select>
<v-select :options="simple" v-model="syncedVal"></v-select>
</div>
<div class="form-group">
@@ -100,10 +100,10 @@
</article>
<article class="doc-row" id="ex-vuex">
<h3 class="page-header">On-Change Callback <small>Vuex Compatibility</small></h3>
<h3 class="page-header">Change Event <small>Vuex Compatibility</small></h3>
<div class="row">
<div class="col-md-6">
<p>vue-select provides an <code>onChange</code> property that accepts a callback function. This function is passed the currently selected value(s) as it's only parameter.</p>
<p>vue-select provides an <code>change</code> event. This function is passed the currently selected value(s) as it's only parameter.</p>
<p>This is very useful when integrating with Vuex, as it will allow your to trigger an action to update your vuex state object. Choose a callback and see it in action.</p>
<div class="form-inline">
@@ -122,7 +122,7 @@
</div>
<div class="col-md-6">
<pre><v-code lang="markup">&#x3C;v-select :on-change=&#x22;consoleCallback&#x22; :options=&#x22;countries&#x22;&#x3E;&#x3C;/v-select&#x3E;</v-code></pre>
<pre><v-code lang="markup">&#x3C;v-select v-on:change=&#x22;consoleCallback&#x22; :options=&#x22;countries&#x22;&#x3E;&#x3C;/v-select&#x3E;</v-code></pre>
<pre><v-code lang="javascript">methods: {
consoleCallback(val) {
console.dir(JSON.stringify(val))
@@ -137,7 +137,7 @@
</div>
</article>
<ajax></ajax>
<!-- <ajax></ajax> -->
</section>
</template>
@@ -157,6 +157,7 @@
export default {
components: {vSelect,vCode,InstallSnippet,Ajax},
// components: {vSelect,vCode,InstallSnippet},
data () {
return {
countries,
+2 -2
View File
@@ -70,7 +70,7 @@
<div>
<div class="github-search panel panel-default">
<div class="panel-heading">
<v-select :debounce="250" :value.sync="repo" :options="options" :on-search="getOptions" placeholder="Search GitHub Repositories..." label="full_name"></v-select>
<v-select :debounce="250" v-model="repo" :options="options" :on-search="getOptions" placeholder="Search GitHub Repositories..." label="full_name"></v-select>
</div>
<div class="panel-body" v-if="repo">
<img :src="repo.owner.avatar_url" alt="{{ repo.owner.login }}" class="gravatar">
@@ -119,4 +119,4 @@
}
}
}
</script>
</script>
+17 -15
View File
@@ -1,16 +1,15 @@
<template>
<h2 class="page-header">Parameters</h2>
<pre v-pre><code class="language-javascript">props: {
<div>
<h2 class="page-header">Parameters</h2>
<pre v-pre><code class="language-javascript">props: {
/**
/**
* Contains the currently selected value. Very similar to a
* `value` attribute on an &amp;lt;input&amp;gt;. In most cases, you'll want
* to set this as a two-way binding, using :value.sync. However,
* this will not work with Vuex, in which case you'll need to use
* the onChange callback property.
* `value` attribute on an <input>. You can listen for changes
* using 'change' event using v-on
* @type {Object||String||null}
*/
value: {
value: {
default: null
},
@@ -90,16 +89,19 @@
* @default {null}
*/
onChange: Function
}
}
</code></pre>
</div>
</template>
<script>
/**
* Note that this file (and anything other than src/components/Select.vue)
* has nothing to do with how you use vue-select. These files are used
* for the demo site at http://sagalbot.github.io/vue-select/. They'll
* be moved out of this repo in the very near future to avoid confusion.
*/
/**
* Note that this file (and anything other than src/components/Select.vue)
* has nothing to do with how you use vue-select. These files are used
* for the demo site at http://sagalbot.github.io/vue-select/. They'll
* be moved out of this repo in the very near future to avoid confusion.
*/
export default {}
</script>
</script>
+7 -6
View File
@@ -113,13 +113,14 @@
</style>
<script type="text/babel">
import GitHubSearchBasic from 'components/GitHubSearchBasic.vue'
import GitHubSearch from 'components/GitHubSearch.vue'
import AjaxProps from './AjaxProps.vue'
import AjaxExample from './AjaxExample.vue'
// import GitHubSearchBasic from 'docs/components/GitHubSearchBasic.vue'
// import GitHubSearch from 'docs/components/GitHubSearch.vue'
// import AjaxProps from './AjaxProps.vue'
// import AjaxExample from './AjaxExample.vue'
export default {
components: {GitHubSearchBasic, GitHubSearch, AjaxProps, AjaxExample},
// components: {GitHubSearchBasic, GitHubSearch, AjaxProps, AjaxExample},
data() {
return {
basicSource: false,
@@ -127,4 +128,4 @@
}
}
}
</script>
</script>
+16 -12
View File
@@ -1,9 +1,10 @@
<template>
<p>Install from GitHub via NPM</p>
<pre><v-code lang="bash">npm install sagalbot/vue-select</v-code></pre>
<div>
<p>Install from GitHub via NPM</p>
<pre><v-code lang="bash">npm install sagalbot/vue-select</v-code></pre>
<p>To use the vue-select component in your templates, simply import it, and register it with your component.</p>
<pre><v-code lang="markup">&#x3C;template&#x3E;
<p>To use the vue-select component in your templates, simply import it, and register it with your component.</p>
<pre><v-code lang="markup">&#x3C;template&#x3E;
&#x3C;div id=&#x22;myApp&#x22;&#x3E;
&#x3C;v-select :value.sync=&#x22;selected&#x22; :options=&#x22;options&#x22;&#x3E;&#x3C;/v-select&#x3E;
&#x3C;/div&#x3E;
@@ -22,16 +23,19 @@
}
&#x3C;/script&#x3E;</v-code>
</pre>
</div>
</template>
<script type="text/babel">
/**
* Note that this file (and anything other than src/components/Select.vue)
* has nothing to do with how you use vue-select. These files are used
* for the demo site at http://sagalbot.github.io/vue-select/. They'll
* be moved out of this repo in the very near future to avoid confusion.
*/
/**
* Note that this file (and anything other than src/components/Select.vue)
* has nothing to do with how you use vue-select. These files are used
* for the demo site at http://sagalbot.github.io/vue-select/. They'll
* be moved out of this repo in the very near future to avoid confusion.
*/
import vCode from '../Code.vue'
export default {
components: {vCode}
components: {
vCode
}
}
</script>
</script>
+4 -5
View File
@@ -25,6 +25,7 @@
<meta property="og:url" content="http://sagalbot.github.io/vue-select/">
</head>
<body>
<div id="app">
<div class="jumbotron jumbotron-top">
<div class="container">
<div class="col-md-8 col-md-offset-2">
@@ -43,12 +44,9 @@
<v-select
id="v-select"
taggable
:placeholder="placeholder"
:value="selected"
:options="options"
:multiple="multiple"
:on-change="setSelected"
multiple
>
</v-select>
@@ -79,7 +77,8 @@
<i role="presentation" class="glyphicon glyphicon-chevron-down"></i>
</a>
</div>
<app></app>
<docs></docs>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+9 -22
View File
@@ -1,10 +1,11 @@
import 'prismjs'
import Vue from 'vue'
import App from './Docs.vue'
import Docs from './Docs.vue'
import store from './vuex/store'
import Resource from 'vue-resource'
import vSelect from '../src/components/Select.vue'
import vCode from './components/Code.vue'
import countries from './data/advanced'
Vue.use(Resource)
@@ -23,27 +24,13 @@ import { setSelected, toggleMultiple, setPlaceholder, toggleOptionType } from '.
/* eslint-disable no-new */
new Vue({
el: 'body',
el: '#app',
store,
components: { App },
vuex: {
getters: {
placeholder (store) {
return store.placeholder
},
selected (store) {
return store.selected
},
type (store) {
return store.optionType
},
options (store) {
return store.options[store.optionType]
},
multiple (store) {
return store.multiple
}
},
actions: { setSelected, toggleMultiple, setPlaceholder, toggleOptionType }
components: { Docs },
data () {
return {
options: countries,
placeholder: 'Choose a country..',
}
}
})
+31
View File
@@ -0,0 +1,31 @@
## AJAX Remote Option Loading
The `onSearch` prop allows you to load options via ajax in a parent component when the search text is updated. It is invoked with two parameters, `search` & `loading`.
#### onSearch Callback Parameters <small>search, loading</small>
`search` is a string containing the current search text. `loading` is a function that accepts a boolean value, and is used to toggle the 'loading' class on the top-level vue-select wrapper.
#### Loading Spinner
Vue Select includes a default loading spinner that appears when the loading class is present. The `spinner` slot allows you to implement your own spinner.
<div id="spinner-example" :class="{loading:spinner}"><button class="btn btn-sm btn-default" @click="spinner = !spinner">Toggle Spinner</button>
<div class="spinner" v-show="spinner">Loading...</div>
#### Debounce Input
Vue Select also accepts a `debounce` prop that can be used to prevent `onSearch` from being called until input has completed.
#### Library Agnostic
Since Vue.js does not ship with ajax functionality as part of the core library, it's up to you to process the ajax requests in your parent component.
#### Example <small>GitHub API</small>
In this example, [Vue Resource](https://github.com/vuejs/vue-resource) is used to access the [GitHub API](https://developer.github.com/v3/).
<git-hub-search-basic></git-hub-search-basic><ajax-example></ajax-example></div>
+28
View File
@@ -0,0 +1,28 @@
```html
<v-select
:debounce="250"
:on-search="getOptions"
:options="options"
placeholder="Search GitHub Repositories..."
label="full_name"
>
</v-select>
```
```js
data() {
return {
options: null
}
},
methods: {
getOptions(search, loading) {
loading(true)
this.$http.get('https://api.github.com/search/repositories', {
q: search
}).then(resp => {
this.options = resp.data.items
loading(false)
})
}
}
```
+24
View File
@@ -0,0 +1,24 @@
```js
/**
* Accept a callback function that will be run
* when the search text changes. The callback
* will be invoked with these parameters:
* @param {search} String Current search text
* @param {loading} Function(bool) Toggle loading class
*/
onSearch: {
type: Function,
default: false
},
/**
* Milliseconds to wait before invoking this.onSearch().
* Used to prevent sending an AJAX request until input
* has completed.
*/
debounce: {
type: Number,
default: 0
}
```
+9
View File
@@ -0,0 +1,9 @@
### Custom Labels
By default when the `options` array contains objects, `vue-select` looks for the `label` key for display. If your data source doesn't contain that key, you can set your own using the `label` prop.
On this page, the list of countries used in the examples contains `value` and `label` properties: `{value: "CA", label: "Canada"}`. In this example, we'll display the country code instead of the label.
`<v-select label="value" :options="countries"></v-select>`
<v-select label="value" :options="countries"></v-select>
View File
+45
View File
@@ -0,0 +1,45 @@
## NPM Based WorkFlows
``` bash
$ npm install vue-select
```
```html
<template>
<div id="myApp">
<v-select v-model="selected" :options="options"></v-select>
</div>
</template>
<script>
import vSelect from 'vue-select'
export default {
components: {vSelect},
data() {
return {
selected: null,
options: ['foo','bar','baz']
}
}
}
</script>
```
## Browser Globals
`v1.3.0+` no longer requires any toolchain to use the component:
Just include `vue` & `vue-select.js` - I recommend using [unpkg.com](https://unpkg.com/#/).
```html
<!-- use the latest release -->
<script src="https://unpkg.com/vue-select@latest"></script>
<!-- or point to a specific release -->
<script src="https://unpkg.com/vue-select@1.30"></script>
```
Then register the component in your javascript:
```js
Vue.component('v-select', VueSelect.VueSelect);
```
From there you can use as normal. Here's an [example on JSBin](http://jsbin.com/saxaru/5/edit?html,js,output).
+26
View File
@@ -0,0 +1,26 @@
### Change Event <small>Vuex Compatibility</small>
vue-select provides a `change` event. This function is passed the currently selected value(s) as it's only parameter.
This is very useful when integrating with Vuex, as it will allow your to trigger an action to update your vuex state object. Choose a callback and see it in action.
<div class="form-inline">
<div class="radio"><label><input type="radio" v-model="callback" value="console"> `console.log(val)`</label> </div>
<div class="radio"><label><input type="radio" v-model="callback" value="alert"> `alert(val)`</label> </div>
</div>
```html
<v-select v-on:change="consoleCallback" :options="countries"></v-select>
```
```js
methods: {
consoleCallback(val) {
console.dir(JSON.stringify(val))
},
alertCallback(val) {
alert(JSON.stringify(val))
}
}
```
+19
View File
@@ -0,0 +1,19 @@
### Reactive Options
When the list of options provided by the parent changes, vue-select will react as you'd expect.
<div style="margin-top:0;" class="radio">
<label>
<input type="radio" name="reactive-options" v-model="reactive" :value="countries">
`<v-select :options="countries"></v-select>`
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="reactive-options" v-model="reactive" :value="['foo','bar','baz']">
`<v-select options="['foo','bar','baz']"></v-select>`
</label>
</div>
<v-select :options="reactive"></v-select>
+29
View File
@@ -0,0 +1,29 @@
<article class="doc-row" id="ex-multiple">
### Single/Multiple Selection
<div class="row">
<div class="col-md-6">
#### Single Option Select
```html
<v-select :options="countries"></v-select>
```
</div>
<div class="col-md-6">
#### Multiple Option Select
```html
<v-select multiple :options="countries"></v-select>
```
</div>
</div>
</article>
+15
View File
@@ -0,0 +1,15 @@
### Two-Way Value Syncing
The most common use case for vue-select is being able to sync the components value with a parent component. The `value` property supports two-way data binding to accomplish this. The `.sync` data-binding modifier is completely optional. You may use `value` without a two-way binding to preselect options. Here we have preselected 'Canada' by setting `syncedVal: 'Canada'` on the parent component. The buttons below demonstrate how you can set the `value` from the parent. Current value: <v-code>{{ syncedVal }}</v-code>
<div class="form-group">
`<v-select v-model="syncedVal" :options="countries"></v-select>`
</div>
<div class="form-group">
<v-select v-model="syncedVal" :options="countries"></v-select>
</div>
<div class="form-group">
<button @click="syncedVal = 'United States'" class="btn btn-default">Set to United States</button>
<button @click="syncedVal = 'Canada'" class="btn btn-default">Set to Canada</button>
</div>
-15
View File
@@ -1,15 +0,0 @@
export const setSelected = ({ dispatch }, selected) => {
dispatch('SET_SELECTED', selected)
}
export const toggleOptionType = ({ dispatch }) => {
dispatch('TOGGLE_OPTION_TYPE')
}
export const setPlaceholder = ({ dispatch }, placeholder) => {
dispatch('SET_PLACEHOLDER', placeholder)
}
export const toggleMultiple = ({ dispatch }) => {
dispatch('TOGGLE_MULTIPLE')
}
-48
View File
@@ -1,48 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
Vue.config.debug = true
const state = {
selected: null,
placeholder: 'Select a Country',
multiple: true,
maxHeight: '400px',
options: {
advanced: require('../data/advanced.js'),
simple: require('../data/simple.js'),
},
optionType: 'advanced'
}
const mutations = {
SET_SELECTED (state, selected) {
state.selected = selected
},
TOGGLE_OPTION_TYPE (state) {
if( state.optionType === 'advanced' ) {
state.optionType = 'simple'
} else {
state.optionType = 'advanced'
}
},
SET_PLACEHOLDER (state, placeholder) {
state.placeholder = placeholder
},
TOGGLE_MULTIPLE (state) {
state.multiple = ! state.multiple
},
SET_MAX_HEIGHT (state, maxHeight) {
state.maxHeight = maxHeight
}
}
export default new Vuex.Store({
state,
mutations
})
+16 -7
View File
@@ -1,6 +1,6 @@
{
"name": "vue-select",
"version": "1.3.3",
"version": "2.0.0",
"description": "A native Vue.js component that provides similar functionality to Select2 without the overhead of jQuery.",
"author": "Jeff Sagal <sagalbot@gmail.com>",
"private": false,
@@ -8,6 +8,7 @@
"license": "MIT",
"scripts": {
"dev": "node build/dev-server.js",
"dev:docs": "node build/dev-server.js --docs",
"build": "node build/build.js",
"lint": "eslint --ext .js,.vue src test/unit/specs",
"test": "karma start test/unit/karma.conf.js --single-run",
@@ -23,6 +24,9 @@
"type": "git",
"url": "https://github.com/sagalbot/vue-select.git"
},
"peerDependencies": {
"vue": "2.x"
},
"devDependencies": {
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
@@ -38,6 +42,8 @@
"file-loader": "^0.8.4",
"function-bind": "^1.0.2",
"gh-pages": "^0.11.0",
"highlight.js": "^9.9.0",
"html-loader": "^0.4.4",
"html-webpack-plugin": "^2.8.1",
"http-proxy-middleware": "^0.15.2",
"inject-loader": "^2.0.1",
@@ -52,6 +58,7 @@
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^1.7.0",
"lolex": "^1.4.0",
"markdown-loader": "^0.1.7",
"node-sass": "^3.7.0",
"ora": "^0.2.0",
"phantomjs-prebuilt": "^2.1.3",
@@ -59,13 +66,15 @@
"sass-loader": "^3.2.0",
"shelljs": "^0.7.0",
"url-loader": "^0.5.7",
"vue": "^1.0.24",
"vue-hot-reload-api": "^1.2.0",
"vue-html-loader": "^1.0.0",
"vue-loader": "^8.3.0",
"vue-resource": "^0.8.0",
"vue": "^2.1.8",
"vue-hot-reload-api": "^2.0.7",
"vue-html-loader": "^1.2.3",
"vue-loader": "^10.0.2",
"vue-markdown-loader": "^0.6.1",
"vue-resource": "^1.0.3",
"vue-style-loader": "^1.0.0",
"vuex": "^0.6.3",
"vue-template-compiler": "^2.1.8",
"vuex": "^2.1.1",
"webpack": "^1.12.2",
"webpack-dev-middleware": "^1.4.0",
"webpack-hot-middleware": "^2.6.0",
+679 -562
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,12 +1,17 @@
import Vue from 'vue'
import vSelect from '../src/components/Select.vue'
import vSelect from './components/Select.vue'
import countries from 'docs/data/advanced.js'
Vue.component('v-select', vSelect)
Vue.config.debug = true
Vue.config.devtools = true
/* eslint-disable no-new */
new Vue({
el: 'body'
el: '#app',
data: {
placeholder: "placeholder",
value: null,
options: countries
}
})
+4 -4
View File
@@ -23,7 +23,7 @@ module.exports = {
*/
onSearch: {
type: Function,
default: false
default: function(search, loading){}
},
/**
@@ -59,9 +59,9 @@ module.exports = {
*/
toggleLoading(toggle = null) {
if (toggle == null) {
return this.loading = !this.loading
return this.showLoading = !this.showLoading
}
return this.loading = toggle
return this.showLoading = toggle
}
}
}
}
+11 -7
View File
@@ -1,3 +1,5 @@
// flow
module.exports = {
watch: {
typeAheadPointer() {
@@ -30,8 +32,10 @@ module.exports = {
*/
pixelsToPointerTop() {
let pixelsToPointerTop = 0
for (let i = 0; i < this.typeAheadPointer; i++) {
pixelsToPointerTop += this.$els.dropdownMenu.children[i].offsetHeight
if( this.$refs.dropdownMenu ) {
for (let i = 0; i < this.typeAheadPointer; i++) {
pixelsToPointerTop += this.$refs.dropdownMenu.children[i].offsetHeight
}
}
return pixelsToPointerTop
},
@@ -50,7 +54,7 @@ module.exports = {
* @returns {number}
*/
pointerHeight() {
let element = this.$els.dropdownMenu.children[this.typeAheadPointer]
let element = this.$refs.dropdownMenu ? this.$refs.dropdownMenu.children[this.typeAheadPointer] : false
return element ? element.offsetHeight : 0
},
@@ -60,8 +64,8 @@ module.exports = {
*/
viewport() {
return {
top: this.$els.dropdownMenu.scrollTop,
bottom: this.$els.dropdownMenu.offsetHeight + this.$els.dropdownMenu.scrollTop
top: this.$refs.dropdownMenu ? this.$refs.dropdownMenu.scrollTop: 0,
bottom: this.$refs.dropdownMenu ? this.$refs.dropdownMenu.offsetHeight + this.$refs.dropdownMenu.scrollTop : 0
}
},
@@ -71,7 +75,7 @@ module.exports = {
* @returns {*}
*/
scrollTo(position) {
return this.$els.dropdownMenu.scrollTop = position
return this.$refs.dropdownMenu ? this.$refs.dropdownMenu.scrollTop = position : null
},
}
}
}
+3
View File
@@ -64,6 +64,9 @@ module.exports = function (config) {
webpackMiddleware: {
noInfo: true
},
specReporter: {
suppressSkipped: true
},
coverageReporter: {
dir: './coverage',
reporters: [
+289 -150
View File
@@ -1,15 +1,15 @@
// flow
/* global describe, it, expect */
import Vue from 'vue'
import vSelect from 'src/components/Select.vue'
// import vSelect from '../../../dist/vue-select'
import pointerScroll from 'src/mixins/pointerScroll.js'
Vue.component('v-select', vSelect)
// http://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
const Mock = require('!!vue?inject!src/components/Select.vue')
Vue.component('v-select', vSelect)
/**
* Simulate a DOM event.
* @param target
@@ -25,6 +25,13 @@ function trigger(target, event, process) {
return e
}
/**
* Simulate a Mouse event.
* @param target
* @param event
* @param process
* @returns {Event}
*/
function triggerMouse(target, event, process) {
var e = document.createEvent('MouseEvent')
e.initEvent('event', true, true)
@@ -33,6 +40,13 @@ function triggerMouse(target, event, process) {
return e
}
/**
* Simulate a Focus event.
* @param target
* @param event
* @param process
* @returns {Event}
*/
function triggerFocusEvent(target, event, process) {
var e = document.createEvent('FocusEvent')
e.initEvent('event', true, true)
@@ -51,7 +65,7 @@ function searchSubmit(vm, search = false) {
vm.$children[0].search = search
}
trigger(vm.$children[0].$els.search, 'keyup', function (e) {
trigger(vm.$children[0].$refs.search, 'keyup', function (e) {
e.keyCode = 13
})
}
@@ -61,67 +75,75 @@ describe('Select.vue', () => {
describe('Selecting values', () => {
it('can accept an array with pre-selected values', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" :value="value"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
value: 'one',
options: ['one', 'two', 'three']
}
}).$mount()
expect(vm.$children[0].value).toEqual(vm.value)
expect(vm.$children[0].mutableValue).toEqual(vm.value)
})
it('can accept an array of objects and pre-selected value (single)', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" :value="value"></v-select></div>',
components: {vSelect},
data: {
value: {label: 'This is Foo', value: 'foo'},
options: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}]
}
}).$mount()
expect(vm.$children[0].value).toEqual(vm.value)
expect(vm.$children[0].mutableValue).toEqual(vm.value)
})
it('can accept an array of objects and pre-selected values (multiple)', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true"></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}],
options: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}]
}
}).$mount()
expect(vm.$children[0].value).toEqual(vm.value)
expect(vm.$children[0].mutableValue).toEqual(vm.value)
})
it('can deselect a pre-selected object', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true"></v-select></div>',
data: {
value: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}],
options: [{label: 'This is Foo', value: 'foo'}, {label: 'This is Bar', value: 'bar'}]
}
}).$mount()
vm.$children[0].select({label: 'This is Foo', value: 'foo'})
expect(vm.$children[0].value.length).toEqual(1)
expect(vm.$children[0].mutableValue.length).toEqual(1)
})
it('can deselect a pre-selected string', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true"></v-select></div>',
data: {
value: ['foo', 'bar'],
options: ['foo','bar']
}
}).$mount()
vm.$children[0].select('foo')
expect(vm.$children[0].value.length).toEqual(1)
expect(vm.$children[0].mutableValue.length).toEqual(1)
}),
it('can deselect an option when multiple is false', () => {
const vm = new Vue({
template: `<div><v-select :value="'foo'"></v-select></div>`,
}).$mount()
vm.$children[0].deselect('foo')
expect(vm.$children[0].mutableValue).toEqual(null)
})
it('can determine if the value prop is empty', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" :value="value"></v-select></div>',
components: {vSelect},
data: {
value: [],
@@ -131,28 +153,28 @@ describe('Select.vue', () => {
var select = vm.$children[0]
expect(select.isValueEmpty).toEqual(true)
select.$set('value', ['one'])
select.select(['one'])
expect(select.isValueEmpty).toEqual(false)
select.$set('value', [{l: 'f'}])
select.select([{l: 'f'}])
expect(select.isValueEmpty).toEqual(false)
select.$set('value', 'one')
select.select('one')
expect(select.isValueEmpty).toEqual(false)
select.$set('value', {label: 'foo', value: 'foo'})
select.select({label: 'foo', value: 'foo'})
expect(select.isValueEmpty).toEqual(false)
select.$set('value', '')
select.select('')
expect(select.isValueEmpty).toEqual(true)
select.$set('value', null)
select.select(null)
expect(select.isValueEmpty).toEqual(true)
})
it('should reset the selected values when the multiple property changes', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="multiple"></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="multiple"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
@@ -164,10 +186,10 @@ describe('Select.vue', () => {
vm.multiple = false
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual(null)
expect(vm.$children[0].mutableValue).toEqual(null)
vm.multiple = true
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual([])
expect(vm.$children[0].mutableValue).toEqual([])
done()
})
})
@@ -175,20 +197,20 @@ describe('Select.vue', () => {
it('can retain values present in a new array of options', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" v-model="value"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
options: ['one', 'two', 'three']
}
}).$mount()
vm.$children[0].$set('options', ['one', 'five', 'six'])
expect(vm.$children[0].value).toEqual(['one'])
vm.options = ['one', 'five', 'six']
expect(vm.$children[0].mutableValue).toEqual(['one'])
})
it('can determine if an object is already selected', () => {
const vm = new Vue({
template: '<div><v-select :options="options" multiple :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" multiple v-model="value"></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
@@ -199,59 +221,99 @@ describe('Select.vue', () => {
expect(vm.$children[0].isOptionSelected({label: 'one'})).toEqual(true)
})
describe('onChange Callback', () => {
it('can run a callback when the selection changes', (done) => {
it('can use v-model syntax for a two way binding to a parent component', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" v-model="value"></v-select></div>',
components: {vSelect},
data: {
value: 'foo',
options: ['foo','bar','baz']
}
}).$mount()
expect(vm.$children[0].value).toEqual('foo')
expect(vm.$children[0].mutableValue).toEqual('foo')
vm.$children[0].mutableValue = 'bar'
Vue.nextTick(() => {
expect(vm.value).toEqual('bar')
done()
})
}),
it('can check if a string value is selected when the value is an object and multiple is true', () => {
const vm = new Vue({
template: `<div><v-select multiple :value="[{label: 'foo', value: 'bar'}]"></v-select></div>`,
}).$mount()
expect(vm.$children[0].isOptionSelected('foo')).toEqual(true)
}),
describe('change Event', () => {
it('will trigger the input event when the selection changes', (done) => {
const vm = new Vue({
template: `<div><v-select :value="['foo']" :options="['foo','bar','baz']" :on-change="cb"></v-select></div>`,
components: {vSelect},
methods: {
cb(val) {
}
template: `<div><v-select ref="select" :value="['foo']" :options="['foo','bar','baz']" v-on:input="foo = arguments[0]"></v-select></div>`,
data: {
foo: ''
}
}).$mount()
spyOn(vm.$children[0], 'onChange')
vm.$children[0].select('bar')
vm.$refs.select.select('bar')
Vue.nextTick(() => {
expect(vm.$children[0].onChange).toHaveBeenCalledWith('bar')
vm.$children[0].select('baz')
Vue.nextTick(() => {
expect(vm.$children[0].onChange).toHaveBeenCalledWith('baz')
done()
})
expect(vm.foo).toEqual('bar')
done()
})
})
it('should run onChange when multiple is true and the value changes', (done) => {
it('should run change when multiple is true and the value changes', (done) => {
const vm = new Vue({
template: `<div><v-select v-ref:select :value="['foo']" :options="['foo','bar','baz']" multiple :on-change="cb"></v-select></div>`,
methods: {
cb(val) {
}
template: `<div><v-select ref="select" :value="['foo']" :options="['foo','bar','baz']" multiple v-on:input="foo = arguments[0]"></v-select></div>`,
data: {
foo: ''
}
}).$mount()
spyOn(vm.$children[0], 'onChange')
vm.$children[0].select('bar')
vm.$refs.select.select('bar')
Vue.nextTick(() => {
expect(vm.$children[0].onChange).toHaveBeenCalledWith(['foo','bar'])
vm.$children[0].select('baz')
Vue.nextTick(() => {
expect(vm.$children[0].onChange).toHaveBeenCalledWith(['foo','bar','baz'])
done()
})
expect(vm.foo).toEqual(['foo','bar'])
done()
})
})
})
})
describe('Filtering Options', () => {
it('should filter an array of strings', () => {
const vm = new Vue({
template: `<div><v-select ref="select" :options="['foo','bar','baz']" v-model="value"></v-select></div>`,
data: {value: 'foo'}
}).$mount()
vm.$refs.select.search = 'ba'
expect(vm.$refs.select.filteredOptions).toEqual(['bar','baz'])
})
it('should filter without case-sensitivity', () => {
const vm = new Vue({
template: `<div><v-select ref="select" :options="['Foo','Bar','Baz']" v-model="value"></v-select></div>`,
data: {value: 'foo'}
}).$mount()
vm.$refs.select.search = 'ba'
expect(vm.$refs.select.filteredOptions).toEqual(['Bar','Baz'])
})
it('can filter an array of objects based on the objects label key', () => {
const vm = new Vue({
template: `<div><v-select ref="select" :options="[{label: 'Foo', value: 'foo'}, {label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]" v-model="value"></v-select></div>`,
data: {value: 'foo'}
}).$mount()
vm.$refs.select.search = 'ba'
expect(JSON.stringify(vm.$refs.select.filteredOptions)).toEqual(JSON.stringify([{label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]))
})
})
describe('Toggling Dropdown', () => {
it('should not open the dropdown when the el is clicked but the component is disabled', (done) => {
const vm = new Vue({
@@ -274,7 +336,7 @@ describe('Select.vue', () => {
it('should open the dropdown when the el is clicked', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" :value="value"></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
@@ -282,7 +344,7 @@ describe('Select.vue', () => {
}
}).$mount()
vm.$children[0].toggleDropdown({target: vm.$children[0].$els.search})
vm.$children[0].toggleDropdown({target: vm.$children[0].$refs.search})
Vue.nextTick(() => {
Vue.nextTick(() => {
expect(vm.$children[0].open).toEqual(true)
@@ -297,20 +359,20 @@ describe('Select.vue', () => {
components: {vSelect},
}).$mount()
spyOn(vm.$children[0].$els.search, 'blur')
spyOn(vm.$children[0].$refs.search, 'blur')
vm.$children[0].open = true
vm.$children[0].toggleDropdown({target: vm.$children[0].$el})
Vue.nextTick(() => {
expect(vm.$children[0].$els.search.blur).toHaveBeenCalled()
expect(vm.$children[0].$refs.search.blur).toHaveBeenCalled()
done()
})
})
it('should close the dropdown on search blur', () => {
const vm = new Vue({
template: '<div><v-select :options="options" multiple :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" multiple :value="value"></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
@@ -319,30 +381,55 @@ describe('Select.vue', () => {
}).$mount()
vm.$children[0].open = true
triggerFocusEvent(vm.$children[0].$els.toggle, 'blur')
triggerFocusEvent(vm.$children[0].$refs.toggle, 'blur')
expect(vm.$children[0].open).toEqual(true)
})
it('will close the dropdown and emit the search:blur event from onSearchBlur', () => {
const vm = new Vue({
template: '<div><v-select></v-select></div>',
}).$mount()
spyOn(vm.$children[0], '$emit')
vm.$children[0].open = true
vm.$children[0].onSearchBlur()
expect(vm.$children[0].open).toEqual(false)
expect(vm.$children[0].$emit).toHaveBeenCalledWith('search:blur')
})
it('will open the dropdown and emit the search:focus event from onSearchFocus', () => {
const vm = new Vue({
template: '<div><v-select></v-select></div>',
}).$mount()
spyOn(vm.$children[0], '$emit')
vm.$children[0].onSearchFocus()
expect(vm.$children[0].open).toEqual(true)
expect(vm.$children[0].$emit).toHaveBeenCalledWith('search:focus')
})
it('will close the dropdown on escape, if search is empty', (done) => {
const vm = new Vue({
template: '<div><v-select></v-select></div>',
components: {vSelect},
}).$mount()
spyOn(vm.$children[0].$els.search, 'blur')
spyOn(vm.$children[0].$refs.search, 'blur')
vm.$children[0].open = true
vm.$children[0].onEscape()
Vue.nextTick(() => {
expect(vm.$children[0].$els.search.blur).toHaveBeenCalled()
expect(vm.$children[0].$refs.search.blur).toHaveBeenCalled()
done()
})
})
it('should remove existing search text on escape keyup', () => {
const vm = new Vue({
template: '<div><v-select :options="options" multiple :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" multiple :value="value"></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
@@ -393,7 +480,7 @@ describe('Select.vue', () => {
vm.$children[0].typeAheadPointer = 1
trigger(vm.$children[0].$els.search, 'keydown', (e) => e.keyCode = 38)
trigger(vm.$children[0].$refs.search, 'keydown', (e) => e.keyCode = 38)
expect(vm.$children[0].typeAheadPointer).toEqual(0)
})
@@ -407,7 +494,7 @@ describe('Select.vue', () => {
}).$mount()
vm.$children[0].typeAheadPointer = 1
trigger(vm.$children[0].$els.search, 'keydown', (e) => e.keyCode = 40)
trigger(vm.$children[0].$refs.search, 'keydown', (e) => e.keyCode = 40)
expect(vm.$children[0].typeAheadPointer).toEqual(2)
})
@@ -437,7 +524,7 @@ describe('Select.vue', () => {
vm.$children[0].typeAheadPointer = 1
spyOn(vm.$children[0], 'maybeAdjustScroll')
trigger(vm.$children[0].$els.search, 'keydown', (e) => e.keyCode = 38)
trigger(vm.$children[0].$refs.search, 'keydown', (e) => e.keyCode = 38)
expect(vm.$children[0].maybeAdjustScroll).toHaveBeenCalled()
})
@@ -451,7 +538,7 @@ describe('Select.vue', () => {
}).$mount()
spyOn(vm.$children[0], 'maybeAdjustScroll')
trigger(vm.$children[0].$els.search, 'keydown', (e) => e.keyCode = 40)
trigger(vm.$children[0].$refs.search, 'keydown', (e) => e.keyCode = 40)
expect(vm.$children[0].maybeAdjustScroll).toHaveBeenCalled()
})
@@ -496,21 +583,26 @@ describe('Select.vue', () => {
expect(vm.$children[0].scrollTo).toHaveBeenCalledWith(1)
})
it('should scroll down if the pointer is below the current viewport bounds', () => {
/**
* @link https://github.com/vuejs/vue-loader/issues/434
* @todo vue-loader/inject-loader fails when used twice in the same file,
* so the mock here is abastracted to a separate file.
*/
xit('should scroll down if the pointer is below the current viewport bounds', () => {
let methods = Object.assign(pointerScroll.methods, {
pixelsToPointerBottom() {
return 2
},
viewport() {
return {top: 0, bottom: 1}
}
pixelsToPointerBottom() {
return 2
},
viewport() {
return {top: 0, bottom: 1}
}
})
const vm = new Vue({
template: '<div><v-select :options="[\'one\', \'two\', \'three\']"></v-select></div>',
template: `<div><v-select :options="['one', 'two', 'three']"></v-select></div>`,
components: {
'v-select': Mock({
'../mixins/pointerScroll': {methods}
})
'v-select': Mock({
'../mixins/pointerScroll': {methods}
})
},
}).$mount()
@@ -523,19 +615,23 @@ describe('Select.vue', () => {
describe('Measuring pixel distances', () => {
it('should calculate pointerHeight as the offsetHeight of the pointer element if it exists', () => {
const vm = new Vue({
template: '<div><v-select :options="[\'one\', \'two\', \'three\']""></v-select></div>',
components: {vSelect},
template: `<div><v-select :options="['one', 'two', 'three']"></v-select></div>`,
}).$mount()
// Fresh instances start with the pointer at -1
vm.$children[0].typeAheadPointer = -1
expect(vm.$children[0].pointerHeight()).toEqual(0)
// dropdown must be open for $refs to exist
vm.$children[0].open = true
vm.$children[0].typeAheadPointer = 100
expect(vm.$children[0].pointerHeight()).toEqual(0)
Vue.nextTick(() => {
// Fresh instances start with the pointer at -1
vm.$children[0].typeAheadPointer = -1
expect(vm.$children[0].pointerHeight()).toEqual(0)
vm.$children[0].typeAheadPointer = 1
expect(vm.$children[0].pointerHeight()).toEqual(vm.$children[0].$els.dropdownMenu.children[1].offsetHeight)
vm.$children[0].typeAheadPointer = 100
expect(vm.$children[0].pointerHeight()).toEqual(0)
vm.$children[0].typeAheadPointer = 1
expect(vm.$children[0].pointerHeight()).toEqual(vm.$children[0].$refs.dropdownMenu.children[1].offsetHeight)
})
})
})
})
@@ -543,16 +639,16 @@ describe('Select.vue', () => {
describe('Removing values', () => {
it('can remove the given tag when its close icon is clicked', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select :options="options" v-model="value" :multiple="true"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
options: ['one', 'two', 'three']
}
}).$mount()
vm.$children[0].$els.toggle.querySelector('.close').click()
vm.$children[0].$refs.toggle.querySelector('.close').click()
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual([])
expect(vm.$children[0].mutableValue).toEqual([])
done()
})
})
@@ -560,7 +656,7 @@ describe('Select.vue', () => {
it('should remove the last item in the value array on delete keypress when multiple is true', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select :options="options" v-model="value" :multiple="true"></v-select></div>',
components: {vSelect},
data: {
value: ['one', 'two'],
@@ -569,13 +665,13 @@ describe('Select.vue', () => {
}).$mount()
vm.$children[0].maybeDeleteValue()
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual(['one'])
expect(vm.$children[0].mutableValue).toEqual(['one'])
})
})
it('should set value to null on delete keypress when multiple is false', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" v-model="value"></v-select></div>',
components: {vSelect},
data: {
value: 'one',
@@ -584,7 +680,7 @@ describe('Select.vue', () => {
}).$mount()
vm.$children[0].maybeDeleteValue()
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual(null)
expect(vm.$children[0].mutableValue).toEqual(null)
})
})
})
@@ -592,14 +688,14 @@ describe('Select.vue', () => {
describe('Labels', () => {
it('can generate labels using a custom label key', () => {
const vm = new Vue({
template: '<div><v-select label="name" :options="options" :value.sync="value" :multiple="true"></v-select></div>',
template: '<div><v-select label="name" :options="options" v-model="value" :multiple="true"></v-select></div>',
components: {vSelect},
data: {
value: [{name: 'Baz'}],
options: [{name: 'Foo'}, {name: 'Baz'}]
}
}).$mount()
expect(vm.$children[0].$els.toggle.querySelector('.selected-tag').textContent).toContain('Baz')
expect(vm.$children[0].$refs.toggle.querySelector('.selected-tag').textContent).toContain('Baz')
})
it('should display a placeholder if the value is empty', (done) => {
@@ -612,20 +708,18 @@ describe('Select.vue', () => {
}).$mount()
expect(vm.$children[0].searchPlaceholder).toEqual('foo')
vm.$children[0].value = {label: 'one'}
vm.$children[0].mutableValue = {label: 'one'}
Vue.nextTick(() => {
expect(vm.$children[0].searchPlaceholder).not.toBeDefined()
done()
})
// expect(vm.$children[0].searchPlaceholder()).toEqual('foo')
})
})
describe('When Tagging Is Enabled', () => {
it('can determine if a given option string already exists', () => {
const vm = new Vue({
template: '<div><v-select v-ref:select :options="options" taggable></v-select></div>',
template: '<div><v-select ref="select" :options="options" taggable></v-select></div>',
components: {vSelect},
data: {
options: ['one', 'two']
@@ -638,7 +732,7 @@ describe('Select.vue', () => {
it('can determine if a given option object already exists', () => {
const vm = new Vue({
template: '<div><v-select v-ref:select :options="options" taggable></v-select></div>',
template: '<div><v-select ref="select" :options="options" taggable></v-select></div>',
components: {vSelect},
data: {
options: [{label: 'one'}, {label: 'two'}]
@@ -651,7 +745,7 @@ describe('Select.vue', () => {
it('can determine if a given option object already exists when using custom labels', () => {
const vm = new Vue({
template: '<div><v-select v-ref:select :options="options" label="foo" taggable></v-select></div>',
template: '<div><v-select ref="select" :options="options" label="foo" taggable></v-select></div>',
components: {vSelect},
data: {
options: [{foo: 'one'}, {foo: 'two'}]
@@ -664,7 +758,7 @@ describe('Select.vue', () => {
it('can add the current search text as the first item in the options list', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" taggable></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
@@ -678,7 +772,7 @@ describe('Select.vue', () => {
it('can select the current search text as a string', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" taggable></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
@@ -688,14 +782,14 @@ describe('Select.vue', () => {
searchSubmit(vm, 'three')
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual(['one', 'three'])
expect(vm.$children[0].mutableValue).toEqual(['one', 'three'])
done()
})
})
it('can select the current search text as an object', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" taggable></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
@@ -705,14 +799,14 @@ describe('Select.vue', () => {
searchSubmit(vm, 'two')
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual([{label: 'one'}, {label: 'two'}])
expect(vm.$children[0].mutableValue).toEqual([{label: 'one'}, {label: 'two'}])
done()
})
})
it('should add a freshly created option/tag to the options list when pushTags is true', () => {
const vm = new Vue({
template: '<div><v-select :options="options" push-tags :value.sync="value" :multiple="true" taggable></v-select></div>',
template: '<div><v-select :options="options" push-tags :value="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
@@ -721,12 +815,12 @@ describe('Select.vue', () => {
}).$mount()
searchSubmit(vm, 'three')
expect(vm.$children[0].options).toEqual(['one', 'two', 'three'])
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two', 'three'])
})
it('wont add a freshly created option/tag to the options list when pushTags is false', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" :taggable="true"></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true" :taggable="true"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
@@ -735,14 +829,13 @@ describe('Select.vue', () => {
}).$mount()
searchSubmit(vm, 'three')
expect(vm.$children[0].options).toEqual(['one', 'two'])
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two'])
})
it('should select an existing option if the search string matches a string from options', (done) => {
let two = 'two'
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
data: {
value: null,
options: ['one', two]
@@ -753,7 +846,7 @@ describe('Select.vue', () => {
searchSubmit(vm)
Vue.nextTick(() => {
expect(vm.$children[0].value[0]).toBe(two)
expect(vm.$children[0].mutableValue[0]).toBe(two)
done()
})
})
@@ -762,7 +855,6 @@ describe('Select.vue', () => {
let two = {label: 'two'}
const vm = new Vue({
template: '<div><v-select :options="options" taggable></v-select></div>',
components: {vSelect},
data: {
options: [{label: 'one'}, two]
}
@@ -775,80 +867,127 @@ describe('Select.vue', () => {
// This needs to be wrapped in nextTick() twice so that filteredOptions can
// calculate after setting the search text, and move the typeAheadPointer index to 0.
Vue.nextTick(() => {
expect(vm.$children[0].value.label).toBe(two.label)
expect(vm.$children[0].mutableValue.label).toBe(two.label)
done()
})
})
})
it('should not reset the selected value when the options property changes', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" :multiple="true" taggable></v-select></div>',
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
options: [{label: 'one'}]
}
}).$mount()
vm.$children[0].options = [{label: 'two'}]
vm.$children[0].mutableOptions = [{label: 'two'}]
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual([{label: 'one'}])
expect(vm.$children[0].mutableValue).toEqual([{label: 'one'}])
done()
})
})
it('should not allow duplicate tags when using string options', (done) => {
const vm = new Vue({
template: `<div><v-select ref="select" taggable multiple></v-select></div>`,
}).$mount()
vm.$refs.select.search = 'one'
searchSubmit(vm)
Vue.nextTick(() => {
expect(vm.$refs.select.mutableValue).toEqual(['one'])
expect(vm.$refs.select.search).toEqual('')
vm.$refs.select.search = 'one'
searchSubmit(vm)
Vue.nextTick(() => {
expect(vm.$refs.select.mutableValue).toEqual([])
expect(vm.$refs.select.search).toEqual('')
done()
})
})
})
it('should not allow duplicate tags when using object options', (done) => {
const vm = new Vue({
template: `<div><v-select ref="select" taggable multiple></v-select></div>`,
}).$mount()
vm.$refs.select.search = 'one'
searchSubmit(vm)
Vue.nextTick(() => {
expect(vm.$refs.select.mutableValue).toEqual(['one'])
expect(vm.$refs.select.search).toEqual('')
vm.$refs.select.search = 'one'
searchSubmit(vm)
Vue.nextTick(() => {
expect(vm.$refs.select.mutableValue).toEqual([])
expect(vm.$refs.select.search).toEqual('')
done()
})
})
})
})
describe('Asynchronous Loading', () => {
it('can toggle the loading class', () => {
const vm = new Vue({
template: '<div><v-select v-ref:select></v-select></div>',
template: '<div><v-select ref="select"></v-select></div>',
}).$mount()
vm.$refs.select.toggleLoading()
expect(vm.$refs.select.loading).toEqual(true)
expect(vm.$refs.select.showLoading).toEqual(true)
vm.$refs.select.toggleLoading(true)
expect(vm.$refs.select.loading).toEqual(true)
expect(vm.$refs.select.showLoading).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>',
template: '<div><v-select ref="select" :on-search="foo"></v-select></div>',
data: {
called: false
},
methods: {
foo() {
foo(val) {
this.called = val
}
}
}).$mount()
spyOn(vm.$refs.select, 'onSearch')
vm.$refs.select.search = 'foo'
Vue.nextTick(() => {
expect(vm.$refs.select.onSearch).toHaveBeenCalledWith('foo', vm.$refs.select.toggleLoading)
expect(vm.called).toEqual('foo')
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>',
template: '<div><v-select ref="select" search="foo" :on-search="foo"></v-select></div>',
data: { called: false },
methods: {
foo() {
foo(val) {
this.called = ! this.called
}
}
}).$mount()
spyOn(vm.$refs.select, 'onSearch')
vm.$refs.select.search = ''
vm.$refs.select.search = 'foo'
Vue.nextTick(() => {
expect(vm.$refs.select.onSearch).not.toHaveBeenCalled()
done()
expect(vm.called).toBe(true)
vm.$refs.select.search = ''
Vue.nextTick(() => {
expect(vm.called).toBe(true)
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>',
template: '<div><v-select loading ref="select" :on-search="foo"></v-select></div>',
methods: {
foo(search, loading) {
loading(false)
@@ -858,14 +997,14 @@ describe('Select.vue', () => {
vm.$refs.select.search = 'foo'
Vue.nextTick(() => {
expect(vm.$refs.select.loading).toEqual(false)
expect(vm.$refs.select.showLoading).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>',
template: '<div><v-select loading ref="select" :on-search="foo"></v-select></div>',
methods: {
foo(search, loading) {
loading(true)
@@ -877,7 +1016,7 @@ describe('Select.vue', () => {
select.onSearch(select.search, select.toggleLoading)
Vue.nextTick(() => {
expect(vm.$refs.select.loading).toEqual(true)
expect(vm.$refs.select.showLoading).toEqual(true)
done()
})
})
@@ -886,31 +1025,31 @@ describe('Select.vue', () => {
describe('Reset on options change', () => {
it('should not reset the selected value by default when the options property changes', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value"></v-select></div>',
template: '<div><v-select :options="options" :value="value"></v-select></div>',
data: {
value: 'one',
options: ['one', 'two', 'three']
}
}).$mount()
vm.$children[0].options = ['four', 'five', 'six']
vm.$children[0].mutableOptions = ['four', 'five', 'six']
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual('one')
expect(vm.$children[0].mutableValue).toEqual('one')
done()
})
})
it('should reset the selected value when the options property changes', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value.sync="value" reset-on-options-change></v-select></div>',
template: '<div><v-select :options="options" :value="value" reset-on-options-change></v-select></div>',
components: {vSelect},
data: {
value: 'one',
options: ['one', 'two', 'three']
}
}).$mount()
vm.$children[0].options = ['four', 'five', 'six']
vm.$children[0].mutableOptions = ['four', 'five', 'six']
Vue.nextTick(() => {
expect(vm.$children[0].value).toEqual(null)
expect(vm.$children[0].mutableValue).toEqual(null)
done()
})
})