mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
Add API for overwriting default components (#850)
* implement API for overwriting child components * add test coverage * update documentation for Components & Styling * update docs * refactor API, update docs * remove the service worker * fix tests
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-select
|
||||||
|
:options="['Canada', 'United States']"
|
||||||
|
:components="{Deselect}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
Deselect () {
|
||||||
|
return Vue.component('Deselect', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('button', 'Clear');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-select
|
||||||
|
class="style-chooser"
|
||||||
|
placeholder="Choose a Styling Option"
|
||||||
|
:options="['Components', 'CSS / Variables', 'Slots']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.style-chooser .vs__search::placeholder,
|
||||||
|
.style-chooser .vs__dropdown-toggle,
|
||||||
|
.style-chooser .vs__dropdown-menu {
|
||||||
|
background: #dfe5fb;
|
||||||
|
border: none;
|
||||||
|
color: #394066;
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-chooser .vs__clear,
|
||||||
|
.style-chooser .vs__open-indicator {
|
||||||
|
fill: #394066;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<custom-select :options="['Vue.js', 'React', 'Angular']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import vSelect from '../../../src/components/Select';
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
Deselect: Vue.component('Deselect', {
|
||||||
|
render: (createElement) => createElement('button', '❌'),
|
||||||
|
}),
|
||||||
|
OpenIndicator: Vue.component('OpenIndicator', {
|
||||||
|
render: (createElement) => createElement('span', '🔽'),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mySelect = {...vSelect};
|
||||||
|
|
||||||
|
mySelect.props.components.default = () => components;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {'custom-select': mySelect}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-select
|
||||||
|
multiple
|
||||||
|
v-model="selected"
|
||||||
|
:options="['Canada', 'United States']"
|
||||||
|
:components="{Deselect}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
selected: ['Canada']
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
Deselect () {
|
||||||
|
return Vue.component('Deselect', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('button', 'Clear');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-select
|
||||||
|
multiple
|
||||||
|
v-model="selected"
|
||||||
|
:options="['Canada', 'United States']"
|
||||||
|
:components="{OpenIndicator}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
selected: ['Canada']
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
OpenIndicator () {
|
||||||
|
return Vue.component('OpenIndicator', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('i', '🤘🏻');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -68,11 +68,8 @@ module.exports = {
|
|||||||
ga: isDeployPreview ? '' : 'UA-12818324-8',
|
ga: isDeployPreview ? '' : 'UA-12818324-8',
|
||||||
},
|
},
|
||||||
'@vuepress/pwa': {
|
'@vuepress/pwa': {
|
||||||
serviceWorker: true,
|
serviceWorker: false,
|
||||||
updatePopup: {
|
updatePopup: true,
|
||||||
message: 'New content is available.',
|
|
||||||
buttonText: 'Refresh',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'@vuepress/plugin-register-components': {},
|
'@vuepress/plugin-register-components': {},
|
||||||
'@vuepress/plugin-active-header-links': {},
|
'@vuepress/plugin-active-header-links': {},
|
||||||
@@ -100,11 +97,19 @@ module.exports = {
|
|||||||
['guide/upgrading', 'Upgrading 2.x to 3.x'],
|
['guide/upgrading', 'Upgrading 2.x to 3.x'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Templating & Styling',
|
||||||
|
collapsable: false,
|
||||||
|
children: [
|
||||||
|
['guide/components', 'Child Components'],
|
||||||
|
['guide/css', 'CSS & Selectors'],
|
||||||
|
['guide/slots', 'Slots'],
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Digging Deeper',
|
title: 'Digging Deeper',
|
||||||
collapsable: false,
|
collapsable: false,
|
||||||
children: [
|
children: [
|
||||||
['guide/templating', 'Templating'],
|
|
||||||
['guide/vuex', 'Vuex'],
|
['guide/vuex', 'Vuex'],
|
||||||
['guide/ajax', 'AJAX'],
|
['guide/ajax', 'AJAX'],
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
::: tip
|
::: tip
|
||||||
VueSelect leverages scoped slots to allow for total customization of the presentation layer.
|
Vue Select leverages scoped slots to allow for total customization of the presentation layer.
|
||||||
Slots can be used to change the look and feel of the UI, or to simply swap out text.
|
Slots can be used to change the look and feel of the UI, or to simply swap out text.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Vue Select utilizes child components throughout, and exposes an API to overwrite these components
|
||||||
|
with your own, using the `components` `{Object}` prop. When implementing the `components` prop in
|
||||||
|
your code, Vue Select merge it's default components with any keys that you set in the object.
|
||||||
|
|
||||||
|
Your object will be merged with the object that is exported below:
|
||||||
|
|
||||||
|
<<< @/src/components/childComponents.js{4-7}
|
||||||
|
|
||||||
|
You can override the value of any of these keys with your own components.
|
||||||
|
|
||||||
|
## Available Components
|
||||||
|
|
||||||
|
### Deselect
|
||||||
|
|
||||||
|
You may wish to roll your own deselect button. `Deselect` is used within tags on
|
||||||
|
`multiple` selects, and serves as the clear button for single selects. Maybe you just want to use
|
||||||
|
a simple `<button>Clear</button>` instead.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<v-select :components="{Deselect}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
computed: {
|
||||||
|
Deselect() {
|
||||||
|
return Vue.component('Deselect', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('button', 'Clear')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<ClearButtonOverride />
|
||||||
|
|
||||||
|
The same approach applies for `multiple` selects:
|
||||||
|
|
||||||
|
<MultipleClearButtonOverride />
|
||||||
|
|
||||||
|
### OpenIndicator
|
||||||
|
|
||||||
|
The `OpenIndicator` component is the 'caret' used within the component that adjusts orientation
|
||||||
|
based on whether the dropdown is open or closed.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<v-select :components="{OpenIndicator}" />
|
||||||
|
```
|
||||||
|
```js
|
||||||
|
computed: {
|
||||||
|
OpenIndicator () {
|
||||||
|
return Vue.component('OpenIndicator', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('button', '🤘🏻');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
<OpenIndicatorOverride />
|
||||||
|
|
||||||
|
## Setting at Registration
|
||||||
|
|
||||||
|
If you want to all instances of Vue Select to use your custom components throughout your app, while
|
||||||
|
only having to set the implementation once, you can do so when registering Vue Select as a component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Vue from 'vue';
|
||||||
|
import vSelect from 'vue-select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom components to override defaults.
|
||||||
|
* @type {{OpenIndicator: *, Deselect: *}}
|
||||||
|
*/
|
||||||
|
const components = {
|
||||||
|
Deselect: Vue.component('Deselect', {
|
||||||
|
render: (createElement) => createElement('button', '❌'),
|
||||||
|
}),
|
||||||
|
OpenIndicator: Vue.component('OpenIndicator', {
|
||||||
|
render: (createElement) => createElement('span', '🔽'),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the components prop default to return our fresh components
|
||||||
|
vSelect.props.components.default = () => components;
|
||||||
|
|
||||||
|
// Register the component
|
||||||
|
Vue.component(vSelect)
|
||||||
|
```
|
||||||
|
|
||||||
|
<CustomComponentRegistration />
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
Vue Select offers many APIs for customizing the look and feel from the component. You can use
|
||||||
|
[scoped slots](../api/slots.md), [custom child components](components.md), or modify the built in
|
||||||
|
SCSS variables.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Support for CSS variables (custom properties) is currently on the road map for those
|
||||||
|
that are not using sass in their projects.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## SCSS Variables
|
||||||
|
|
||||||
|
Variables are leveraged in as much of the component styles as possible. If you really want to dig
|
||||||
|
into the SCSS, the files are located in `src/scss`. The variables listed below can be found at
|
||||||
|
[`src/scss/global/_variables`](https://github.com/sagalbot/vue-select/blob/master/src/scss/global/_variables.scss).
|
||||||
|
|
||||||
|
All variables are implemented with `!default` in order to make them easier to override in your
|
||||||
|
application.
|
||||||
|
|
||||||
|
<<< @/src/scss/global/_variables.scss
|
||||||
|
|
||||||
|
## Overriding Default Styles
|
||||||
|
|
||||||
|
Vue Select takes the approach of using selectors with a single level of specificity, while using
|
||||||
|
classes that are very specific to Vue Select to avoid collisions with your app.
|
||||||
|
|
||||||
|
All classes within Vue Select use the `vs__` prefix, and selectors are generally a single classname
|
||||||
|
– unless there is a state being applied to the component.
|
||||||
|
|
||||||
|
In order to override a default property in your app, you should add one level of specificity.
|
||||||
|
The easiest way to do this, is to add `.v-select` before the `vs__*` selector if you want to adjust
|
||||||
|
all instances of Vue Select, or add your own classname if you just want to affect one.
|
||||||
|
|
||||||
|
<CssSpecificity />
|
||||||
|
|
||||||
|
<<< @/docs/.vuepress/components/CssSpecificity.vue
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
::: tip 🚧
|
||||||
|
This section of the guide is a work in progress! Check back soon for an update.
|
||||||
|
Vue Select currently offers quite a few scoped slots, and you can check out the
|
||||||
|
[API Docs for Slots](../api/slots.md) in the meantime while a good guide is put together.
|
||||||
|
:::
|
||||||
|
|
||||||
#### Scoped Slot `option`
|
#### Scoped Slot `option`
|
||||||
|
|
||||||
vue-select provides the scoped `option` slot in order to create custom dropdown templates.
|
vue-select provides the scoped `option` slot in order to create custom dropdown templates.
|
||||||
@@ -13,3 +13,10 @@
|
|||||||
[context.deploy-preview]
|
[context.deploy-preview]
|
||||||
publish = "docs/.vuepress/dist"
|
publish = "docs/.vuepress/dist"
|
||||||
command = "yarn && yarn build:preview"
|
command = "yarn && yarn build:preview"
|
||||||
|
|
||||||
|
|
||||||
|
# Redirects
|
||||||
|
# @see https://www.netlify.com/docs/netlify-toml-reference/
|
||||||
|
[[redirects]]
|
||||||
|
from = "/guide/templating"
|
||||||
|
to = "/guide/slots"
|
||||||
|
|||||||
+42
-11
@@ -18,7 +18,7 @@
|
|||||||
{{ getOptionLabel(option) }}
|
{{ getOptionLabel(option) }}
|
||||||
</slot>
|
</slot>
|
||||||
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" aria-label="Deselect option">
|
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" aria-label="Deselect option">
|
||||||
<deselect />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
@@ -37,10 +37,12 @@
|
|||||||
class="vs__clear"
|
class="vs__clear"
|
||||||
title="Clear selection"
|
title="Clear selection"
|
||||||
>
|
>
|
||||||
<deselect />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<open-indicator v-if="!noDrop" ref="openIndicator" role="presentation" class="vs__open-indicator" />
|
<slot name="open-indicator-icon">
|
||||||
|
<component :is="childComponents.OpenIndicator" v-bind="scope.openIndicator.attributes"/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
<slot name="spinner" v-bind="scope.spinner">
|
<slot name="spinner" v-bind="scope.spinner">
|
||||||
<div class="vs__spinner" v-show="mutableLoading">Loading...</div>
|
<div class="vs__spinner" v-show="mutableLoading">Loading...</div>
|
||||||
@@ -75,11 +77,10 @@
|
|||||||
import pointerScroll from '../mixins/pointerScroll'
|
import pointerScroll from '../mixins/pointerScroll'
|
||||||
import typeAheadPointer from '../mixins/typeAheadPointer'
|
import typeAheadPointer from '../mixins/typeAheadPointer'
|
||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import Deselect from './Deselect'
|
import childComponents from './childComponents';
|
||||||
import OpenIndicator from './OpenIndicator'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {Deselect, OpenIndicator},
|
components: {...childComponents},
|
||||||
|
|
||||||
mixins: [pointerScroll, typeAheadPointer, ajax],
|
mixins: [pointerScroll, typeAheadPointer, ajax],
|
||||||
|
|
||||||
@@ -92,6 +93,18 @@
|
|||||||
*/
|
*/
|
||||||
value: {},
|
value: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object with any custom components that you'd like to overwrite
|
||||||
|
* the default implementation of in your app. The keys in this object
|
||||||
|
* will be merged with the defaults.
|
||||||
|
* @see https://vue-select.org/guide/components.html
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
components: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of strings or objects to be used as dropdown choices.
|
* 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
|
* If you are using an array of objects, vue-select will look for
|
||||||
@@ -443,10 +456,6 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone props into mutable values,
|
|
||||||
* attach any event listeners.
|
|
||||||
*/
|
|
||||||
created() {
|
created() {
|
||||||
this.mutableLoading = this.loading;
|
this.mutableLoading = this.loading;
|
||||||
|
|
||||||
@@ -878,7 +887,29 @@
|
|||||||
},
|
},
|
||||||
spinner: {
|
spinner: {
|
||||||
loading: this.mutableLoading
|
loading: this.mutableLoading
|
||||||
}
|
},
|
||||||
|
openIndicator: {
|
||||||
|
attributes: {
|
||||||
|
'v-if': !this.noDrop,
|
||||||
|
'ref': 'openIndicator',
|
||||||
|
'role': 'presentation',
|
||||||
|
'class': 'vs__open-indicator',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object containing the child components
|
||||||
|
* that will be used throughout the component. The
|
||||||
|
* `component` prop can be used to overwrite the defaults.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
childComponents () {
|
||||||
|
return {
|
||||||
|
...childComponents,
|
||||||
|
...this.components
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Deselect from './Deselect';
|
||||||
|
import OpenIndicator from './OpenIndicator';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Deselect,
|
||||||
|
OpenIndicator
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import { selectWithProps } from '../helpers';
|
||||||
|
|
||||||
|
describe('Components API', () => {
|
||||||
|
|
||||||
|
it('swap the Deselect component', () => {
|
||||||
|
const Deselect = Vue.component('Deselect', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('button', 'remove');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Select = selectWithProps({components: {Deselect}});
|
||||||
|
|
||||||
|
expect(Select.contains(Deselect)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swap the OpenIndicator component', () => {
|
||||||
|
const OpenIndicator = Vue.component('OpenIndicator', {
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('i', '^');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Select = selectWithProps({components: {OpenIndicator}});
|
||||||
|
|
||||||
|
expect(Select.contains(OpenIndicator)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -58,7 +58,7 @@ describe("VS - Selecting Values", () => {
|
|||||||
|
|
||||||
const spy = jest.spyOn(Select.vm, "typeAheadSelect");
|
const spy = jest.spyOn(Select.vm, "typeAheadSelect");
|
||||||
|
|
||||||
Select.find({ ref: "search" }).trigger("keyup.tab");
|
Select.find({ ref: "search" }).trigger("keydown.tab");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith();
|
expect(spy).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user