I wanted to add some own scenarios for new components by forking vue-play.
I'm having problems in more complicated cases of vue-select, particularly Two-Way Value Syncing.
Going into this scenario ends up with warning:
vue.esm.js:571 [Vue warn]: Property or method "syncedVal" is not
defined on the instance but referenced during render.
and no option in the dropdown is preselected either. I'm failing to understand why I keep getting this warning, despite defining syncedVal in component's props.
I've added two files into vue-play/play:
VSelect.vue:
<template>
<v-select v-model="selected" :options="options"></v-select>
</template>
<script>
import Vue from 'vue'
import vSelect from 'vue-select'
Vue.component('v-select', vSelect);
export default {
props: {
options: {
default: function() { return ['one', 'two'] },
type: Array
},
onchangeCallback: {
default: () => () => null
},
// doesn't seem to work:
syncedVal: {
default: 'one',
type: String
}
},
data() {
return {
selected: null
}
}
}
</script>
and VSelect.play.js:
import {play} from '../src/play'
import VSelect from './VSelect.vue'
play(VSelect)
.name('VSelect')
.displayName('VSelect')
.add('default', '<v-select />')
.add('multiple', '<v-select multiple />')
.add('custom options', `<v-select :options="['custom1','custom2']" />`)
.add('custom options with labels', `<v-select :options='[{value: "CA", label: "Canada"}, {value: "UK", label: "United Kingdom"}]' />`)
.add('2-way value sync', `<v-select :value.sync="syncedVal" />`) // doesn't seem to work
please note that the v-select component in VSelect.play.js is VSelect.vue
so there are some mistakes:
.add('multiple', '<v-select multiple />')
VSelect.vue do not have multiple props, so this multiple will not work as you expectd
FIX: add props to your component, and bind it to v-select
.add('2-way value sync', )
you define syncedVal in component's props, BUT you use it on other component (vue-play's component), they have different scope!
FIX: to use vue-play to demo this functionality, you need to write a full component, so you can have data to bind(see below for example code)
VSelect.vue do not implement sync (https://v2.vuejs.org/v2/guide/components.html#sync-Modifier), so nothing will happen here
I make an example from your component and play, I hope this will help you :)
Demo of my vue-play: https://vue-play-select.netlify.com/
Code: https://github.com/iampaul83/vue-play-select
here is how I fix them:
SelectFramework.vue:
<template>
<v-select
v-model="selected"
:options="options"
:multiple="multiple">
</v-select>
</template>
<script>
export default {
name: "SelectFramework",
props: {
options: {
default: () => ["Vue.js", "React", "Angular"],
type: Array
},
value: String, // to support v-model
foo: String, // to support :foo.sync
multiple: false // to support multiple select
},
data() {
return {
selected: this.value,
};
},
watch: {
selected () {
this.$emit('input', this.selected) // update v-model
this.$emit('update:foo', this.selected) // update foo.sync
},
value () {
this.selected = this.value // update from v-model
},
foo () {
this.selected = this.foo // update from foo.sync
}
}
};
</script>
play/index.js:
import { play } from 'vue-play'
import Vue from 'vue'
import vSelect from 'vue-select'
Vue.component('v-select', vSelect)
import SelectFramework from '../src/SelectFramework.vue'
play(SelectFramework)
.name('SelectFramework')
.displayName('Select Framework')
.add('default', '<select-framework />')
.add('multiple', '<select-framework multiple />')
.add('custom options', `<select-framework :options="['custom1','custom2']" />`)
.add('custom options with labels', `<select-framework :options='[{value: "CA", label: "Canada"}, {value: "UK", label: "United Kingdom"}]' />`)
// full component to demo v-model and :foo.sync
.add('v-model', {
data() {
return {
selected: null,
syncedVal: null
}
},
template: `
<div>
<p>selected: {{selected}} </p>
<p>syncedVal: {{syncedVal}} </p>
<select-framework
v-model="selected"
:foo.sync="syncedVal">
</select-framework>
<p>
<button #click="selected = 'Vue.js'">Set selected to Vue.js</button>
<button #click="syncedVal = 'React'">Set syncedVal to React</button>
</p>
</div>
`
})
// .add('2-way value sync', `<select-framework :value.sync="syncedVal" />`) // doesn't seem to work
Related
I'm despaired. I absolutely have no clue anymore how to outsource html "select"-tag to a single Vue component file. I tried so much different ways but unfortunately I've never got it to work yet.
User component:
<custom-select :id="'continent'" :selected="user.continent" :options="continents" #change.native="changedContinent" class="some css classes" />
export default {
props: ['user'],
components: {
CustomSelect,
},
data() {
return {
continents: [
{ value: 'africa', text: 'Africa' },
{ value: 'america-north', text: 'America (North)' },
{ value: 'america-south', text: 'America (South)' },
{ value: 'asia', text: 'Asia' },
{ value: 'australia', text: 'Australia' },
{ value: 'europe', text: 'Europe' }
]
};
},
methods: {
updateUser() {
axios.post('/updateUser', this.user).then(() => {});
},
changedContinent() {
// Do something with the new selected continent like changing background relative to the selected continent
}
}
};
CustomSelect:
<template>
<select v-model="selected" class="some css classes">
<option v-for="option in options" :key="option.value" :value="option.value">{{ option.text }}</option>
</select>
</template>
<script>
export default { props: ['id', 'selected', 'label', 'options'] };
</script>
Whenever I change the selected value it should update user.continent value but instead, it throws the following error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"selected"
But I do not know how to handle this.
Could anyone help me please?Thank you in advance!
As the error message says, 'to avoid mutating a prop directly, use a data or computed property ...'
I modified your code to create the components in my test environment (Vue 2 using Vue CLI), and bound the CustomSelect to a new data property 'selectedContinent' initialized from the 'selected' prop. I am also updating the parent component (User component in your case) via a custom event. Here are my components. See the Vue Custom Events documentation
My CustomSelect.vue component:
<template>
<div class="custom-select">
<select v-model="selectedContinent" class="" v-on:change="changeContinent">
<option v-for="option in options" :key="option.value" :value="option.value">{{ option.text }}</option>
</select>
</div>
</template>
<script>
export default {
props: ['id', 'selected', 'label', 'options'],
data() {
return {
selectedContinent: this.selected
}
},
methods: {
changeContinent() {
//console.log('changeContinent: ' + this.selectedContinent);
this.$emit('change-continent-event', this.selectedContinent);
}
}
}
</script>
My Parent.vue component (your User):
<template>
<div class="parent">
<custom-select :id="'continent'" :selected="user.continent" :options="continents"
v-on:change-continent-event="changedContinent" />
</div>
</template>
<script>
import CustomSelect from './CustomSelect.vue'
export default {
props: ['user'],
components: {
CustomSelect,
},
data() {
return {
continents: [
{ value: 'africa', text: 'Africa' },
{ value: 'america-north', text: 'America (North)' },
{ value: 'america-south', text: 'America (South)' },
{ value: 'asia', text: 'Asia' },
{ value: 'australia', text: 'Australia' },
{ value: 'europe', text: 'Europe' }
]
};
},
methods: {
updateUser() {
//axios.post('/updateUser', this.user).then(() => { });
},
changedContinent(newContinent) {
// Do something with the new selected continent like changing background relative to the selected continent
console.log('changedContinent: ' + newContinent);
}
}
}
</script>
I currently set up a new playground with VueJS/Laravel/Spark and want to implement a tags input component.
I don't understand how to register those components correctly. I'm following the how-to-guides and official documentation, but the implementation just works so-so.
I want to implement the library from #johmun -> http://www.vue-tags-input.com which I installed via npm (npm install #johmun/vue-tags-input).
I created a single file component named VueTagsInput.vue that looks like this:
<template>
<div>
<vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
export default {
components: {
VueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>
I imported this single file component at resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
And I'm using this component in the home.blade.php view like this:
<vue-tags-input v-model="tag"
autocomplete-always-open
add-from-paste
allow-edit-tags>
</vue-tags-input>
This renders an input with which I can interact as desired, but I can not use the autocomplete function with the countries entered above, and the console also throws the following error:
[Vue warn]: Property or method "tag" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
I don't know what I'm doing wrong.
So I stumbled across the solution by trial & error.
First I had to register the component the right way in resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
Vue.component('vue-tags-input', VueTagsInput);
But this caused another error because I called the component within the component registration itself. I used the name option in the single file component in order to overcome this error. I gave my newly created component a different name like this:
<template>
<div>
<johmun-vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import JohmunVueTagsInput from '#johmun/vue-tags-input';
export default {
name: "VueTagsInput",
components: {
JohmunVueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>
I have a parent component with 2 child components that both inherit from the same base component. (This parent component is being created and used in a Vue Storybook). Both SiblingAComponent and SiblingBComponent inherit the same BaseComponent, and instantiate the same inherited data classInstance, which is a vanilla JS class instance from another library. I am trying to access this classInstance from the parent component to pass as data into the second sibling component (in this case, from SiblingAComponent to SiblingBComponent), by using an reference siblingARef. However, I get this error from the storybook compiler:
too much recursion
isArguments#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49010:16
keys#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49073:28
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119972:19
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119974:28
ParentComponent Story:
storiesOf("ParentComponent Story", module)
.addDecorator(
withKnobs({
escapeHTML: false
})
)
.add("Passing data from A to B", () => ({
name: 'ParentComponent',
components: {
SiblingAComponent,
SiblingBComponent,
},
data() {
return {
siblingAData: [....], // array of objects
siblingAOptions: {
axes: {},
height: "50px",
},
siblingBData: [...], // array of objects
siblingBOptions: null,
}
},
mounted() {
const siblingAInstance = this.$refs.siblingARef.classInstance;
const newOptions = {
legend: {
external: {
reference: siblingAInstance,
},
},
};
// this line is where I am getting an error
this.siblingBOptions = legendExternal;
},
template: `
<SiblingAComponent ref="siblingARef" :data="siblingAData" :options="siblingAOptions"/>
<SiblingBComponent v-if="siblingBData" :data="siblingBData" :options="siblingBOptions"/>
`,
}));
SiblingAComponent:
<template>
<div class="sibling-a-component"></div>
</template>
<script>
import { ComponentA } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingAComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentA(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
SiblingBComponent:
<template>
<div class="sibling-b-component"></div>
</template>
<script>
import { ComponentB } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingBComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentB(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
BaseComponent:
<script>
export default {
name: 'BaseComponent',
data() {
return {
classInstance: null,
};
},
props: {
data: { type: [Object, Array], required: true },
options: { type: Object, required: true },
},
};
</script>
Coming from the Angular and React worlds, using a reference to access a Vanilla class instance from another Component is nothing new, even if it's unconventional. I am new to Vue, so I am wondering why would trying to access a class instance fail (works fine for primitive data types) and give me such a weird error? Where is the recursion occurring?
For example: <component v-model='foo' :is='boo' ...>.
foo's value stays the same during input.
I'm trying to solve the issue for quite a long time. I've checked lots of questions and threads but none of those helped me.
HTML doesn't work:
<component
:is="field.component"
:key="key"
:name="field.name"
v-for="(field, key) in integration_data"
v-model="field.value"
>
</component>
HTML works fine:
<input
:key="key"
:name="field.name"
v-for="(field, key) in integration_data"
v-model="field.value"
>
Vue controller:
export default {
init: function (init_data) {
return new Vue({
data: {
integration_data: [
{name: 'field_name0', component: 'input', value: ''},
{name: 'field_name0', component: 'input', value: ''},
]
},
});
}
}
You can't use input as a type of component and expect it to be a native input element. :is must name a component (which can contain an input, if you want).
Then you have to understand how v-model works on components:
So for a component to work with v-model, it should (these can be
configured in 2.2.0+):
accept a value prop
emit an input event with the new value
Putting that all together, you can use v-model with :is.
new Vue({
el: '#app',
data: {
integration_data: [{
name: 'one',
component: 'one',
value: 'ok'
}]
},
components: {
one: {
props: ['name', 'value'],
template: '<div>{{name}} and <input v-model="proxyValue"><slot></slot></div>',
computed: {
proxyValue: {
get() { return this.value; },
set(newValue) { this.$emit('input', newValue); }
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<component
:is="field.component"
v-for="(field, key) in integration_data"
:key="key"
:name="field.name"
v-model="field.value"
>
<div>{{field.value}}</div>
</component>
</div>
v-model has nothing to do with what you're possibly trying to do. It looks like you are trying to insert components dynamically. That's exactly what :is does. Now, to pass data to the component, you should use props.
For example:
Your Vue instance:
const vm = new Vue({
el: '#app',
data: {
exampleValue: 'This value will be passed to the example component'
}
})
Register a component:
Vue.component('example-component', {
// declare the props
props: ['value'],
template: '<span>{{ value}}</span>'
})
Then use it like this:
<example-component :value="exampleValue"></example-component>
Or:
<component :is="'example-component'" :value="exampleValue"></component>
I'm learning Vue and I noticed that I have the following syntax more or less everywhere.
export default {
components: { Navigation, View1 },
computed: {
classObject: function() {
return {
alert: this.$store.state.environment !== "dev",
info: this.$store.state.environment === "dev"
};
}
}
}
It's a pain to write out this.$store.state.donkey all the time and it lowers the readability too. I'm sensing that I'm doing it in a less than optimal way. How should I refer to the state of the store?
you can set computed properties for both states & getters i.e.
computed: {
donkey () {
this.$store.state.donkey
},
ass () {
this.$store.getters.ass
},
...
Whilst you still need to call the $state.store once you can then reference a donkey or an ass on your vm...
To make things even easier you can pull in the vuex map helpers and use them to find your ass ... or donkey:
import { mapState, mapGetters } from 'vuex'
default export {
computed: {
...mapState([
'donkey',
]),
...mapGetters([
'ass',
]),
...mapGetters({
isMyAss: 'ass', // you can also rename your states / getters for this component
}),
now if you look at this.isMyAss you'll find it ... your ass
worth noting here that getters, mutations & actions are global - therefore they are referenced directly on your store, i.e. store.getters, store.commit & store.dispatch respectively. This applies whether they are in a module or in the root of your store. If they are in a module check out namespacing to prevent overwriting previously used names: vuex docs namespacing. However if you are referencing a modules state, you must prepend the name of the module, i.e. store.state.user.firstName in this example user is a module.
Edit 23/05/17
Since the time of writing Vuex has been updated and its namespacing feature is now a go to when you work with modules. Simply add namespace: true to your modules export, i.e.
# vuex/modules/foo.js
export default {
namespace: true,
state: {
some: 'thing',
...
add the foo module to your vuex store:
# vuex/store.js
import foo from './modules/foo'
export default new Vuex.Store({
modules: {
foo,
...
then when you are pulling this module into your components you can:
export default {
computed: {
...mapState('foo', [
'some',
]),
...mapState('foo', {
another: 'some',
}),
...
this makes modules very simple and clean to use, and is a real saviour if you are nesting them multiple levels deep: namespacing vuex docs
I have put together an example fiddle to showcase the various ways you can reference and work with your vuex store:
JSFiddle Vuex Example
Or check out the below:
const userModule = {
state: {
firstName: '',
surname: '',
loggedIn: false,
},
// #params state, getters, rootstate
getters: {
fullName: (state, getters, rootState) => {
return `${state.firstName} ${state.surname}`
},
userGreeting: (state, getters, rootState) => {
return state.loggedIn ? `${rootState.greeting} ${getters.fullName}` : 'Anonymous'
},
},
// #params state
mutations: {
logIn: state => {
state.loggedIn = true
},
setName: (state, payload) => {
state.firstName = payload.firstName
state.surname = payload.surname
},
},
// #params context
// context.state, context.getters, context.commit (mutations), context.dispatch (actions)
actions: {
authenticateUser: (context, payload) => {
if (!context.state.loggedIn) {
window.setTimeout(() => {
context.commit('logIn')
context.commit('setName', payload)
}, 500)
}
},
},
}
const store = new Vuex.Store({
state: {
greeting: 'Welcome ...',
},
mutations: {
updateGreeting: (state, payload) => {
state.greeting = payload.message
},
},
modules: {
user: userModule,
},
})
Vue.component('vuex-demo', {
data () {
return {
userFirstName: '',
userSurname: '',
}
},
computed: {
loggedInState () {
// access a modules state
return this.$store.state.user.loggedIn
},
...Vuex.mapState([
'greeting',
]),
// access modules state (not global so prepend the module name)
...Vuex.mapState({
firstName: state => state.user.firstName,
surname: state => state.user.surname,
}),
...Vuex.mapGetters([
'fullName',
]),
...Vuex.mapGetters({
welcomeMessage: 'userGreeting',
}),
},
methods: {
logInUser () {
this.authenticateUser({
firstName: this.userFirstName,
surname: this.userSurname,
})
},
// pass an array to reference the vuex store methods
...Vuex.mapMutations([
'updateGreeting',
]),
// pass an object to rename
...Vuex.mapActions([
'authenticateUser',
]),
}
})
const app = new Vue({
el: '#app',
store,
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
<!-- inlining the template to make things easier to read - all of below is still held on the component not the root -->
<vuex-demo inline-template>
<div>
<div v-if="loggedInState === false">
<h1>{{ greeting }}</h1>
<div>
<p><label>first name: </label><input type="text" v-model="userFirstName"></p>
<p><label>surname: </label><input type="text" v-model="userSurname"></p>
<button :disabled="!userFirstName || !userSurname" #click="logInUser">sign in</button>
</div>
</div>
<div v-else>
<h1>{{ welcomeMessage }}</h1>
<p>your name is: {{ fullName }}</p>
<p>your firstName is: {{ firstName }}</p>
<p>your surname is: {{ surname }}</p>
<div>
<label>Update your greeting:</label>
<input type="text" #input="updateGreeting({ message: $event.target.value })">
</div>
</div>
</div>
</vuex-demo>
</div>
As you can see if you wanted to pull in mutations or actions this would be done in a similar way but in your methods using mapMutations or mapActions
Adding Mixins
To extend the above behaviour you could couple this with mixins and then you'd only have to set up the above computed properties once and pull in the mixin on the component that needs them:
animals.js (mixin file)
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState([
'donkey',
...
your component
import animalsMixin from './mixins/animals.js'
export default {
mixins: [
animalsMixin,
],
created () {
this.isDonkeyAnAss = this.donkey === this.ass
...