Vue: How to use store with component? - vue.js

//store
export default {
state: {
aboutModels: []
},
actions: {
findBy: ({commit}, about)=> {
//do getModels
var aboutModels = [{name: 'About'}] //Vue.resource('/abouts').get(about)
commit('setModels', aboutModels)
}
},
getters: {
getModels(state){
return state.aboutModels
}
},
mutations: {
setModels: (state, aboutModels)=> {
state.aboutModels = aboutModels
}
}
}
//component
import {mapActions, mapGetters} from "vuex";
export default {
name: 'About',
template: require('./about.template'),
style: require('./about.style'),
created () {
document.title = 'About'
this.findBy()
},
computed: mapGetters({
abouts: 'getModels'
}),
methods: mapActions({
findBy: 'findBy'
})
}
//view
<div class="about" v-for="about in abouts">{{about.name}}</div>
//error
vue.js:2532[Vue warn]: Cannot use v-for on stateful component root element because it renders multiple elements:
<div class="about" v-for="about in abouts">{{about.name}}</div>
vue.js:2532[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node. (found in component <About>)

You are mapping your Vuex state getters and action correctly. Your problem is something else as your error message states...
In your component template you can not use v-for directive on a root element. For example this is not allowed because your component can have multiple root elements:
<template>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</template>
instead do it this way:
<template>
<div>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</div>
</template>
** *fixed typo in template tag **

Related

how to validate child form from parent component in Vue

I have a child component which includes form:
<el-form :model="abc" ref="ruleForm" :rules="rules">
<el-form-item prop="files">
<abc-card :title="getTranslation('abc.files')">
<file-selector v-model="abc.files" />
</abc-card>
</el-form-item>
</el-form>
And I want to add simple validations to this form:
rules: function () {
return {
files: [
{
type: 'object',
required: true,
trigger: 'change',
message: 'Field required',
},
],
};
},
But my click button is in the parent component:
<files v-model="editableAbc" ref="editableTab" />
<el-button type="primary" #click="submitForm()">Create</el-button>
methods: {
submitForm() {
this.$refs.form.validate((isValid) => {
if (!isValid) {
return;
}
////API CALLS////
});
},
}
So I am trying to achieve that when the button is clicked the navigation should be rendered. How can I do that?
As per your requirement, My suggestion would be to use a ref on child component to access its methods and then on submit click in parent component, trigger the child component method.
In parent component template :
<parent-component>
<child-component ref="childComponentRef" />
<button #click="submitFromParent">Submit</button>
</parent-component>
In parent component script :
methods: {
submitFromParent() {
this.$refs.childComponentRef.submitForm();
}
}
In child component script :
methods: {
submitForm() {
// Perform validations and do make API calls based on validation passed.
// If you want to pass success or failure in parent then you can do that by using $emit from here.
}
}
The "files" component is the form you're talking about?
If so, then ref should be placed exactly when calling the 'files' component, and not inside it. This will allow you to access the component in your parent element.
<files v-model="editableAbc" ref="ruleForm" />
There is a method with the props, which was mentioned in the comments above. I really don't like it, but I can tell you about it.
You need to set a value in the data of the parent component. Next you have to pass it as props to the child component. When you click the button, you must change the value of this key (for example +1). In the child component, you need to monitor the change in the props value via watch and call your validation function.
// Parent
<template>
<div class="test">
<ChildComponent />
</div>
</template>
<script>
export default {
data() {
return {
updateCount: 0,
};
},
methods: {
submitForm() {
// yout submit method
this.updateCount += 1;
},
},
};
</script>
// Child
<script>
export default {
props: {
updateCount: {
type: Number,
default: 0,
},
},
watch: {
updateCount: {
handler() {
this.validate();
},
},
},
methods: {
validate() {
// yout validation method
},
},
};
</script>
And one more solution. It is suitable if you cannot place the button in the child component, but you can pass it through the slot.
You need to pass the validate function in the child component through the prop inside the slot. In this case, in the parent component, you will be able to get this function through the v-slot and bind it to your button.
// Parent
<template>
<div class="test">
<ChildComponent>
<template #button="{ validate }">
<button #click="submitForm(validate)">My button</button>
</template>
</ChildComponent>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
components: {
ChildComponent,
},
methods: {
submitForm(cb) {
const isValid = cb();
// your submit code
},
},
};
</script>
// Child
<template>
<div class="child-component">
<!-- your form -->
<slot name="button" :validate="validate" />
</div>
</template>
<script>
export default {
methods: {
validate() {
// yout validation method
console.log("validate");
},
},
};
</script>

Passing data from route to router-view body

I have a single page application with the structure below.
|- App.vue
|- + Views
| |- Page.vue
|- + Components
| |- Slider.vue
EDIT 1: Solution thanks to #gengar.value
I solved the issue by passing params from Page.vue with
methods: {
emitIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
App.vue containing the router-view container that is routing the Page.vue
Slider.vue is a component of App.vue
I want to pass index of clicked image and whole images data from Page.vue to App.vue then to Slider.vue in order to achieve decoupling Slider from Page for reusability purposes.
How can I pass user selected index from Page.vue too App.vue
I have tried to use, params, props and emit but failed.
Sample Page.vue
<template>
<div v-for="(item, index) in 3" :key="index"></div>
</template>
<script>
export default ({
data() {
return {
urls: ['url1', 'url2', 'url3']
}
}
})
</script>
Thanks in advance
EDIT 1: Solution thanks to #gengar.value
Problem solved by pushing params to router via Page.vue and listening it from Slider.vue as follows:
Page.vue
methods: {
passIndex: function (index) {
this.$router.push({
name: "visualization",
params: { imgCat: "visualization", imgIndex: index },
});
},
}
Slider.vue
watch: {
"$route.params.imgCat": function (val) {
this.state = val;
},
"$route.params.imgIndex": function (newVal) {
if (newVal != -1) this.imgState = newVal;
this.$router.push({ params: { imgIndex: -1 } });
}
My solution is a little bit complicated, but quite native since I only used Props and Emit.
You want to pass value between brother components, so you could simply try below:
App.vue
<template>
<div>
<Page :data="data" #syncData="syncData" />
<Slider :data="data" />
</div>
<template>
<script>
import Page from './Views/Page.vue'
import Slider from './Components/Slider.vue'
export default ({
components: {
Page,
Silder
},
data() {
return {
data: [] // init data in the parent component
}
},
methods: {
syncData(updatedImages) {
this.data = updatedImages
}
}
})
</script>
Page.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
methods: {
onSelectImage(images) {
this.$emit('syncData', images) // update selected data to App.vue
}
}
})
</script>
Slider.vue
<template>
<div></div>
<template>
<script>
export default ({
props: {
data: { type: Array, default: () => [] }
},
watch: {
data: {
handler(val) {
// when Page.vue emits updated data to App.vue,
// App.vue will pass data to Slider.vue
// and you could receive the updated data 'val' here
},
deep: true
}
}
})
</script>
Update: Sorry I misunderstood before. If you are using vue-router components and assigned different paths (eg. '/page' and '/slider'), you can use
this.$router.push({ path: '/path', query: selectedImage })
in Page.vue and get url query in Slider.vue.
Alternative methods could be using Cookie.js or sessionStorage (not pretty tho). Also you could try Vuex if the specific condition suits you.

Element UI dialog component can open for the first time, but it can't open for the second time

I'm building web app with Vue, Nuxt, and Element UI.
I have a problem with the Element dialog component.
It can open for the first time, but it can't open for the second time.
This is the GIF about my problem.
https://gyazo.com/dfca3db76c75dceddccade632feb808f
This is my code.
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first :visible=visible></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
ModalFirst.vue
<template>
<el-dialog
title="Tips"
:visible.sync="visible"
width="30%"
>
<span>This is a message</span>
<span slot="footer" class="dialog-footer">
<a>Hello</a>
</span>
</el-dialog>
</template>
<script>
export default {
props: [ 'visible' ]
}
</script>
And I can see a warning message on google chrome console after closing the dialog.
The warning message is below.
webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:620 [Vue warn]: 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: "visible"
found in
---> <ModalFirst> at components/ModalFirst.vue
<Pages/index.vue> at pages/index.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
This is the screenshot of the warning message.
https://gyazo.com/83c5f7c5a8e4d6816c35b3116c80db0d
In vue , using directly to prop value is not allowed . Especially when your child component will update that prop value , in my option if prop will be use
for display only using directly is not a problem .
In your code , .sync will update syncronously update data so I recommend to create local data.
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'visible' ],
data: function () {
return {
localVisible: this.visible // create local data using prop value
}
}
}
</script>
If you need the parent visible property to be updated, you can create your component to leverage v-model:
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'value' ],
data() {
return {
localVisible: null
}
},
created() {
this.localVisible = this.value;
this.$watch('localVisible', (value, oldValue) => {
if(value !== oldValue) { // Optional
this.$emit('input', value); // Required
}
});
}
}
</script>
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first v-model="visible"></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
v-model is basically a shorthand for :value and #input
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage
Side-note:
You can also import your component like so:
components: { ModalFirst },
as ModalFirst will be interpreted as modal-first as well by Vue.js

Error in render: "TypeError: Cannot read property 'fieldName' of undefined"

I am rendering a custom component list (this is just an example) as below:
<template>
<ul>
<li
v-for="(item, index) in componentList"
:field-name="MyFieldName"
>
<custom_component :index="index"
:some-prop="item.someProps" />
></li>
</ul>
</template>
<script>
export default {
name: 'myListComp';
props: {
},
... other props
},
data () {
return {
/// ... some other variable/properties for this instance
myFieldName: '',
componentList: []
}
},
created () {
// ...
Load data from props
this.myFieldName = this.fieldName;
// Initialize Component List with the data
this.componentList = this.initializeComponentList();
// ...
},
methods:{
initializeComponentList() {
// Get the list from AJAX
return [{}, {}, ....];
}
}
}
</script>
I am getting some weird error as below while rendering the component list.
On a side note ... I am using the latest laravel-mix ^2.1.11 for compiling the vue components. I am using Vue ^2.5.16 and vuex ^3.0.1.

Why this.$listeners is undefined in Vue JS?

Vue.js version: 2.4.2
Below component always print this.$listeners as undefined.
module.exports = {
template: `<h1>My Component</h1>`,
mounted() {
alert(this.$listeners);
}
}
I register the component and put it inside a parent component.
Can someone tell me why?
You have to understand what $listeners are.
this.$listeners will be populated once there are components that listen to events that your components is emitting.
let's assume 2 components:
child.vue - emits an event each time something is written to input field.
<template>
<input #input="emitEvent">
</input>
</template>
<script>
export default {
methods: {
emitEvent() {
this.$emit('important-event')
console.log(this.$listeners)
}
}
}
</script>
parent.vue - listen to the events from child component.
<template>
<div class="foo">
<child #important-event="doSomething"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
newcomment: {
post_id: 'this is default value'
}
}
},
components: { child },
methods: {
doSomething() {
// do something
}
}
}
</script>
With this setup, when you type something to the input field, this object should be written to the console:
{
`important-event`: function () { // some native vue.js code}
}
I added the following alias to my webpack.config.js file and this resolved the issue for me:-
resolve: {
alias: {
'vue$': path.resolve(__dirname, 'node_modules/vue/dist/vue.js')
}
},