How to collect selected BootstrapVue (b-form-checkbox) check boxes? - vue.js

I work on Nuxt.js (Vue.js) application and modal dialog looks as following:
The code of Basket.vue component looks as following:
<template lang="pug">
b-container(v-if='products.length > 0')
b-row
b-col(:cols="8")
b-container
b-row
b-list-group
b-list-group-item.flex-column.align-items-start(v-for="(product, index) in products" :key="index")
b-container
b-row
b-col(:cols="1").d-flex.align-items-center.justify-content-center
b-form-checkbox {{ index + 1 }}
b-col(:cols="11")
basket-item(:product="product")
b-row.row-middle-style
b-button.close(type='button' aria-label='Close' #click="onRemoveItem")
span(aria-hidden='true') ×
u Remove selected items
b-button(type='button' #click="onContinue()").btn.btn-lg.buy__button Продолжить покупки
b-row
div
hr
| * доставка будет осуществлена из Санкт-Петербурга;
br
| * наш менеджер свяжется с вами сразу после обработки заказа;
...
</template>
<script lang="ts">
import Vue from 'vue'
import BasketItem from './BasketItem.vue'
import { mapState, mapActions } from 'vuex'
export default Vue.extend({
components: {
BasketItem,
},
data() {
return {
}
},
computed: {
...mapState(['products']),
},
methods: {
...mapActions({
onRemoveItem: 'removeItem',
}),
},
})
</script>
<style lang="sass">
...
</style>
The code of BasketItem.vue component looks as following:
<template lang="pug">
b-container
b-row.style-item-row
b-col(:cols="3")
img(:src="product.image" :alt="product.alt")
b-col(:cols="4")
div {{ product.name }}
div {{ product.description }}
b-col(:cols="4")
div Цена {{ product.price }}
div Количество {{ product.quantity }}
b-col(:cols="1")
b-button.close(type='button' aria-label='Close' #click="onRemoveItem(product.id)")
span(aria-hidden='true') ×
</template>
<script lang="ts">
import Vue from 'vue'
import { mapActions } from 'vuex'
export default Vue.extend({
props: {
product: {
type: Object,
},
},
data() {
return {
}
},
methods: {
...mapActions({
onRemoveItem: 'removeItem',
}),
},
})
</script>
<style lang="sass">
...
</style>
The Vuex code looks as following:
import * as mutationTypes from './mutation_types'
export const state = () => ({
products: [
{
id: 1,
image: 'https://licota.ru/system/product_images/attachments/5d9b/1781/6332/3406/9d00/2a31/small/8bfa7c2c-c7c7-11e4-80f4-002590d99cf6.jpg?1570445184',
alt: 'Товар 1',
name: 'Товар 1',
description: 'Описание 1',
price: 100,
quantity: 4
},
{
id: 2,
image: 'https://licota.ru/system/product_images/attachments/5d9b/329f/6332/3406/9d00/9336/small/e6a69bba-3450-11e9-812c-002590d99cf6.jpg?1570452124',
alt: 'Товар 2',
name: 'Товар 2',
description: 'Описание 2',
price: 200,
quantity: 7
}
]
})
export const getters = {
}
export const actions = {
removeItem({ commit }, id) {
commit(mutationTypes.REMOVE_ITEM, id)
}
}
export const mutations = {
[mutationTypes.REMOVE_ITEM] (state, id) {
state.products = state.products.filter(x => {
return x.id != id
})
}
}
As you can see I have a list of items (there are two). In front of each of them is BootstrapVue (b-form-checkbox) check box. I can select any of them and click "Remove selected items". After that they will be removed. The code for that action doesn't exist at this moment. The Vuex code you can see refers to clicking "x" in upper-right corner of items.
My issue is how to collect selected items that is check boxes that are checked in order to remove appropriate items?
UPDATE:
I've tried you'd suggested, but it doesn't work. The most likely I did something wrong. Actually, when I take a look at Vue section in DevTools, isChecked computed property doesn't change after clicking check boxes. The code looks as following:
Basket.vue:
<template lang="pug">
b-container(v-if='products.length > 0')
b-row
b-col(:cols="8")
b-container
b-row
b-list-group
b-list-group-item.flex-column.align-items-start(
v-for="(product, index) in products"
:key="product.id"
v-bind="{...product}"
v-on="{'update:checked': (data) => handleSetChecked(data)}"
)
b-container.set_pad
b-row
b-col(:cols="12").d-flex.align-items-center.justify-content-center
basket-item(:product="product" :productIndex="index")
b-row.row-middle-style
b-button.close(type='button' aria-label='Close' #click="onRemoveSelectedItems")
span(aria-hidden='true') ×
u Удалить выбранное
b-button(type='button' #click="onContinue()").btn.btn-lg.buy__button Продолжить покупки
b-row
div
hr
| * доставка будет осуществлена из Санкт-Петербурга;
br
| * наш менеджер свяжется с вами сразу после обработки заказа;
...
</template>
<script>
import Vue from 'vue'
import BasketItem from './BasketItem.vue'
import { mapActions } from 'vuex'
export default Vue.extend({
components: {
BasketItem,
},
data() {
return {}
},
computed: {
products() {
return this.$store.getters.getProducts
},
toRemove() {
return this.$store.getters.getProductsToRemove
},
},
methods: {
...mapActions({
onRemoveItem: 'removeItem',
}),
handleSetChecked(data) {
this.$store.dispatch("setToRemove", data)
},
handleRemoveItems() {
this.$store.dispatch("removeSelected")
},
onRemoveSelectedItems() {
this.handleRemoveItems()
},
},
})
</script>
BasketItem.vue:
<template lang="pug">
b-container
b-row.style-item-row
b-col(:cols="1").d-flex.align-items-center.justify-content-center
b-form-checkbox(v-model="isChecked") {{ productIndex + 1 }}
b-col(:cols="3")
img(:src="product.image" :alt="product.alt")
b-col(:cols="3")
div {{ product.title }}
div {{ product.description }}
b-col(:cols="4")
div Цена {{ product.price }}
div Количество {{ product.quantity }}
b-col(:cols="1")
b-button.close(type='button' aria-label='Close' #click="onRemoveItem(product.id)")
span(aria-hidden='true') ×
</template>
<script>
import Vue from 'vue'
import { mapActions } from 'vuex'
export default Vue.extend({
props: {
product: {
type: Object,
},
productIndex: {
type: Number,
},
},
data() {
return {}
},
computed: {
isChecked: {
get() {
return this.product.checked
},
set(val) {
this.$emit("update:checked", {
id: this.product.id,
checked: val,
})
}
},
},
methods: {
...mapActions({
onRemoveItem: 'removeItem',
}),
},
})
</script>
Vuex:
import * as mutationTypes from './mutation_types'
export const state = () => ({
products: [
{
id: 1,
image: 'https://licota.ru/system/product_images/attachments/5d9b/1781/6332/3406/9d00/2a31/small/8bfa7c2c-c7c7-11e4-80f4-002590d99cf6.jpg?1570445184',
alt: 'Товар 1',
title: 'Товар 1',
description: 'Описание 1',
price: 100,
quantity: 4,
checked: false
},
{
id: 2,
image: 'https://licota.ru/system/product_images/attachments/5d9b/329f/6332/3406/9d00/9336/small/e6a69bba-3450-11e9-812c-002590d99cf6.jpg?1570452124',
alt: 'Товар 2',
title: 'Товар 2',
description: 'Описание 2',
price: 200,
quantity: 7,
checked: false
}
]
})
export const getters = {
getProducts: state => state.products,
getProductsToRemove: state => state.products.filter(({
checked
}) => checked),
}
export const actions = {
setToRemove({
commit
}, data) {
commit("SET_TO_REMOVE", data)
},
removeSelected({
commit
}) {
commit("REMOVE_SELECTED")
},
removeItem({ commit }, id) {
commit(mutationTypes.REMOVE_ITEM, id)
},
}
export const mutations = {
SET_TO_REMOVE(state, {
id,
checked
}) {
state.products = state.products.map(product => {
if (product.id === id) {
return {
...product,
checked,
}
} else {
return product
}
})
},
REMOVE_SELECTED(state) {
state.products = state.products.filter(({
checked
}) => !checked)
},
[mutationTypes.REMOVE_ITEM] (state, id) {
state.products = state.products.filter(x => {
return x.id != id
})
},
}
The reference to "Form Checkbox Inputs":
https://bootstrap-vue.org/docs/components/form-checkbox

I advise you to think through your code, and have the logic steps ready:
You put out a list of UI items, based on a list in the Vuex store
Each UI item has a toggle (checkbox) that sets some part of the state of the item it displays (should it be removed or not)
By clicking the toggle (checkbox), the state of the item should change
By clicking the button (remove), the Vuex state should change: remove every item whose state fulfills a condition (it's toRemove state is true)
That's all. More simply put: don't work by selecting some UI element. Work by updating the state of the data that the UI element is based upon. (And let Vue's reactive capability do the rest.)
The first snippet is an easy one, not using Vuex:
Vue.component('RowWithCb', {
props: ["id", "title", "checked"],
computed: {
isChecked: {
get() {
return this.checked
},
set(val) {
this.$emit("update:checked", val)
}
},
},
template: `
<b-row>
<b-col
class="d-flex"
>
<div>
<b-checkbox
v-model="isChecked"
/>
</div>
<div>
{{ title }}
</div>
</b-col>
</b-row>
`
})
new Vue({
el: "#app",
computed: {
toRemove() {
return this.rows.filter(({
checked
}) => checked)
},
toKeep() {
return this.rows.filter(({
checked
}) => !checked)
},
},
data() {
return {
rows: [{
id: 1,
title: "Row 1",
checked: false,
},
{
id: 2,
title: "Row 2",
checked: false,
}
]
}
},
template: `
<b-container>
<b-row>
<b-col>
<h3>Items:</h3>
<row-with-cb
v-for="row in rows"
:key="row.id"
v-bind="{
...row
}"
:checked.sync="row.checked"
/>
</b-col>
</b-row>
<hr />
<b-row>
<b-col>
<h4>To keep:</h4>
{{ toKeep }}
</b-col>
<b-col>
<h4>To remove:</h4>
{{ toRemove }}
</b-col>
</b-row>
</b-container>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>
NOW WITH VUEX
const initialState = () => ({
rows: [{
id: 1,
title: "Row 1",
checked: false,
},
{
id: 2,
title: "Row 2",
checked: false,
}
]
})
const store = new Vuex.Store({
state: initialState(),
mutations: {
RESET(state) {
const init = initialState()
for (key in init) {
state[key] = init[key]
}
},
SET_TO_REMOVE(state, {
id,
checked
}) {
state.rows = state.rows.map(row => {
if (row.id === id) {
return {
...row,
checked,
}
} else {
return row
}
})
},
REMOVE_SELECTED(state) {
state.rows = state.rows.filter(({
checked
}) => !checked)
},
},
actions: {
reset({
commit
}) {
commit("RESET")
},
setToRemove({
commit
}, data) {
commit("SET_TO_REMOVE", data)
},
removeSelected({
commit
}) {
commit("REMOVE_SELECTED")
},
},
getters: {
getRows: state => state.rows,
getRowsToRemove: state => state.rows.filter(({
checked
}) => checked),
getRowsToKeep: state => state.rows.filter(({
checked
}) => !checked),
}
})
Vue.component('RowWithCb', {
props: ["id", "title", "checked"],
computed: {
isChecked: {
get() {
return this.checked
},
set(val) {
this.$emit("update:checked", {
id: this.id,
checked: val,
})
}
},
},
template: `
<b-row>
<b-col
class="d-flex"
>
<div>
<b-checkbox
v-model="isChecked"
/>
</div>
<div>
{{ title }}
</div>
</b-col>
</b-row>
`
})
new Vue({
el: "#app",
store,
computed: {
rows() {
return this.$store.getters.getRows
},
toRemove() {
return this.$store.getters.getRowsToRemove
},
toKeep() {
return this.$store.getters.getRowsToKeep
},
},
methods: {
handleSetChecked(data) {
this.$store.dispatch("setToRemove", data)
},
handleRemoveItems() {
this.$store.dispatch("removeSelected")
},
handleResetStore() {
this.$store.dispatch("reset")
},
},
template: `
<b-container>
<b-row>
<b-col>
<h3>Items:</h3>
<row-with-cb
v-for="row in rows"
:key="row.id"
v-bind="{
...row
}"
v-on="{
'update:checked': (data) => handleSetChecked(data)
}"
/>
</b-col>
</b-row>
<hr />
<b-row>
<b-col>
<h4>To keep:</h4>
{{ toKeep }}
</b-col>
<b-col>
<h4>To remove:</h4>
{{ toRemove }}
</b-col>
</b-row>
<hr />
<b-row>
<b-col>
<b-button
#click="handleRemoveItems"
>
REMOVE
</b-button>
<b-button
#click="handleResetStore"
>
RESET
</b-button>
</b-col>
</b-row>
</b-container>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app"></div>
You can see that I did not mess with any checkboxes or UI elements (the only difference is that the id is also emitted from the UI component). All the functions were moved to the Vuex store & that's all. (A bit more verbose than the first snippet, as I added the remove & the reset functionality.)
LET'S BREAK IT DOWN
1. You need a component that lists items. In this case, let's use the root component for this:
new Vue({
el: "#app",
data() {
return {
baseItems: [{
id: "item_1",
name: "Item 1",
},
{
id: "item_2",
name: "Item 2",
},
]
}
},
computed: {
items() {
return this.baseItems
},
},
template: `
<ul>
<li
v-for="item in items"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>
I deliberately put items as computed - it's going to be ease to switch to Vuex.
2. You know that your listed items are going to be a bit more complex than this, so let's create a component that can handle their upcoming complexity:
Vue.component('ListItem', {
props: ['id', 'name'],
template: `
<li>
{{ name }}
</li>
`
})
new Vue({
el: "#app",
data() {
return {
baseItems: [{
id: "item_1",
name: "Item 1",
},
{
id: "item_2",
name: "Item 2",
},
]
}
},
computed: {
items() {
return this.baseItems
},
},
template: `
<ul>
<list-item
v-for="item in items"
:key="item.id"
v-bind="{
...item,
}"
/>
</ul>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>
3. Let's make them removable - one-by-one or in a bunch:
Vue.component('ListItem', {
props: ['id', 'name', 'toRemove'],
computed: {
selected: {
get() {
return this.toRemove
},
set(val) {
this.$emit('update:to-remove', val)
}
},
},
template: `
<li>
<input
type="checkbox"
v-model="selected"
/>
{{ name }}
</li>
`
})
new Vue({
el: "#app",
data() {
return {
baseItems: [{
id: "item_1",
name: "Item 1",
toRemove: false,
},
{
id: "item_2",
name: "Item 2",
toRemove: false,
},
]
}
},
computed: {
items() {
return this.baseItems
},
},
methods: {
handleRemoveSelected() {
this.baseItems = this.baseItems.filter(item => !item.toRemove)
},
},
template: `
<div>
So, it's easier to follow:
{{ items }}
<hr />
<ul>
<list-item
v-for="item in items"
:key="item.id"
v-bind="{
...item,
}"
:toRemove.sync="item.toRemove"
/>
</ul>
<b-button
#click="handleRemoveSelected"
>
REMOVE SELECTED
</b-button>
</div>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>
OK, now there is the function you were after - without Vuex
4. Let's mix in Vuex:
move the list to a Vuex state
move the selection to a Vuex action
move the removal to a Vuex action
// initial state is a function per Vuex best practices
const initialState = () => ({
items: [{
id: "item_1",
name: "Item 1",
toRemove: false,
},
{
id: "item_2",
name: "Item 2",
toRemove: false,
}
]
})
const store = new Vuex.Store({
state: initialState(),
mutations: {
// so you are able to reset the store to
// initial state
RESET(state) {
const init = initialState()
for (key in init) {
state[key] = init[key]
}
},
// this handles the "checked" state if each item
SET_TO_REMOVE(state, {
id,
toRemove,
}) {
// returning a new, modified array
// per FLUX process
state.items = state.items.map(item => {
if (item.id === id) {
return {
...item,
toRemove,
}
} else {
return item
}
})
},
// the same function that was in the app before
REMOVE_SELECTED(state) {
state.items = state.items.filter(({
toRemove
}) => !toRemove)
},
},
actions: {
reset({
commit
}) {
commit("RESET")
},
// action to commit the mutation: select item for removal
setToRemove({
commit
}, data) {
commit("SET_TO_REMOVE", data)
},
// action to commit the mutation: remove items
removeSelected({
commit
}) {
commit("REMOVE_SELECTED")
},
},
getters: {
getItems: state => state.items,
}
})
Vue.component('ListItem', {
props: ['id', 'name', 'toRemove'],
computed: {
selected: {
get() {
return this.toRemove
},
set(val) {
this.$emit('update:to-remove', val)
}
},
},
template: `
<li>
<input
type="checkbox"
v-model="selected"
/>
{{ name }}
</li>
`
})
new Vue({
el: "#app",
store,
data() {
return {
baseItems: [{
id: "item_1",
name: "Item 1",
toRemove: false,
},
{
id: "item_2",
name: "Item 2",
toRemove: false,
},
]
}
},
computed: {
items() {
return this.$store.getters.getItems
},
},
methods: {
handleSetToRemove(data) {
// data should be: { id, toRemove }
this.$store.dispatch("setToRemove", data)
},
handleRemoveSelected() {
this.$store.dispatch("removeSelected")
},
handleResetStore() {
this.$store.dispatch("reset")
},
},
template: `
<div>
So, it's easier to follow:
{{ items }}
<hr />
<ul>
<list-item
v-for="item in items"
:key="item.id"
v-bind="{
...item,
}"
#update:to-remove="(toRemove) => handleSetToRemove({
id: item.id,
toRemove,
})"
/>
</ul>
<b-button
#click="handleRemoveSelected"
>
REMOVE SELECTED
</b-button>
<b-button
#click="handleResetStore"
>
RESET
</b-button>
</div>
`
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue#latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app"></div>
This is all there's to it.
You handle the selection in the small component - but the change of the selected state is NOT HANDLED inside this component, but emitted to the parent, which in turn handles this by calling a Vuex action. This changes the state in the Vuex store & that "flows" back through the props.
If you click on REMOVE SELECTED, that doesn't look at the components (if they are selected), but calls another Vuex action, that removes all selected items (by filtering) from the state of the Vuex store. As all items are put out from a getter of the Vuex store (and getters update if the underlying state changes), the new state appears in the list.
So, in this case, using Vuex might seem a bit overkill, but if you plan on a more complex state for the listed items, then it may be reasonable.

Related

Vue component prop not updating on $emit

I need to update the component's prop on every route change but it stays with the last info given.
For example, I fill the form in RouteTwo, with id, name, lastname and phone, and if I change to RouteOne, ComponentOne stays with those four values (including phone) until I start filling the form in RouteOne.
I'm working with vue 2.6.12, vue-router 3.4.9
Here's an example code:
General.vue
<template>
<div>
<div>
<router-view #data-updated="updateFunction"></router-view>
</div>
<div>
<component-one v-bind:component-prop="reactiveProp" />
</div>
</div>
</template>
<script>
import ComponentOne from './ComponentOne.vue';
export default {
components: {
ComponentOne,
},
data: () => ({
reactiveProp
}),
methods: {
updateFunction(value) {
this.reactiveProp = value;
},
}
}
</script>
ComponentOne.vue
<template>
<div>
<p>{{ componentProp.id }}</p>
<p>{{ componentProp.name }}</p>
<p>{{ componentProp.lastname }}</p>
<p v-if="routePath == 'routetwo'">{{ componentProp.phone }}</p>
</div>
</template>
<script>
export default {
props: {
componentProp: Object,
},
data: () => ({
routePath: ''
}),
mounted() {
this.routePath = this.$route.path.split('/').at(-1);
},
}
</script>
RouteOne.vue
<template>
<div>
<input type="text" v-model="dataObject.id" />
<input type="text" v-model="dataObject.name" />
<input type="text" v-model="dataObject.lastname" />
</div>
</template>
<script>
export default {
data: () => ({
dataObject: {
id: '',
name: '',
lastname: '',
},
}),
methods: {
// some logic methods
},
watch: {
dataObject: {
handler: function() {
this.$emit('data-updated',this.dataObject);
},
deep: true,
}
},
}
</scipt>
RouteTwo.vue
<template>
<div>
<input type="text" v-model="dataObject.id" />
<input type="text" v-model="dataObject.name" />
<input type="text" v-model="dataObject.lastname" />
<input type="text" v-model="dataObject.phone" />
</div>
</template>
<script>
export default {
data: () => ({
dataObject: {
id: '',
name: '',
lastname: '',
phone: '',
},
}),
methods: {
// some logic methods
},
watch: {
dataObject: {
handler: function() {
this.$emit('data-updated',this.dataObject);
},
deep: true,
}
},
}
</scipt>
Router
{
name: 'general',
path: '/general',
component: () => import('General.vue'),
children: [
{
name: 'route-one',
path: 'routeone',
component: () => import('RouteOne.vue')
},
{
name: 'route-two',
path: 'routetwo',
component: () => import('RouteTwo.vue')
}
]
}
Switching routes doesn't cause a change in dataObject, so no emit will fire. Technically, switching routes causes one route component to be unmounted (destroyed), and the next route component to be created, meaning dataObject is created/recreated every time you switch back and forth, which is fundamentally different from dataObject being "changed" (which activates the watcher).
Instead, put a watcher on the route itself in the General component and reset reactiveProp when that watcher activates. This will clear the fields when going between RouteOne and RouteTwo:
watch: {
$route(to, from) {
this.reactiveProp = {};
},
},

Error: Do not mutate vuex store state outside mutation handlers

Scenario
I am using Vuex, to store some data in it, and in my case the ticket details.
Initially, I have a ticket which has an array of discounts, to be empty.
Once I hit the button "Add discount" I mount the component called "testDiscount" which in the mounted hook pushes the first object ({"code": "Foo", "value":"Boo"}) in the discounts array of a specific ticket in the store.
The problem arise when I try to type in the input boxes (changing its state) in this component where I get the error "do not mutate Vuex store state outside mutation handlers.". How could I best handle this?
Test.vue
<template>
<div>
<test-component v-for="(t, key) in tickets" :key="key" :ticket-key="key" :tid="t.id"></test-component>
</div>
</template>
<script>
import TestComponent from "~/components/testComponent.vue";
export default {
layout: "noFooter",
components: {
"test-component": TestComponent,
},
data() {
return {
tickets: this.$store.state.ticketDiscount.tickets,
};
},
mounted() {
if (this.tickets.length == 0) {
this.$store.commit("ticketDiscount/addTicket", {
id:
this.$store.state.ticketDiscount.tickets.length == 0
? 0
: this.$store.state.ticketDiscount.tickets[
this.$store.state.ticketDiscount.tickets.length - 1
].id + 1,
discount: [],
});
}
},
};
</script>
ticketDiscount.js
export const state = () => ({
tickets: []
});
export const mutations = {
addTicket(state, ticket) {
state.tickets.push(ticket);
},
addDiscount(state, property) {
state.tickets.find(ticket => ticket.id == property.id)[property.name].push(property.value);
}
testComponent.vue
<template>
<div>
<h3>Ticket number: {{ticketKey + 1}}</h3>
<button #click="showDiscount = true">Add discount</button>
<test-discount v-model="discount_" v-if="showDiscount" :tid="tid"></test-discount>
</div>
</template>
<script>
import testDiscount from "~/components/test-discount.vue";
export default {
components: {
testDiscount,
},
data() {
return {
showDiscount: false,
tid_: this.tid,
};
},
props: {
tickets: Array,
ticketKey: { type: Number },
tid: { type: Number, default: 0 },
},
methods: {
updateTicket() {
this.$emit("updateTicket", {
id: this.tid_,
value: {
discount: this.discount_,
},
});
},
},
mounted() {
this.$watch(
this.$watch((vm) => (vm.discount_, Date.now()), this.updateTicket)
);
},
computed: {
discount_: {
get() {
return this.$store.state.ticketDiscount.tickets.find(
(ticket) => ticket.id == this.tid
)["discount"];
},
set(value) {
// set discount
},
},
},
};
</script>
testDiscount.vue
<template>
<div class="container">
<div class="title">
<img src="~/assets/svgs/price_tag.svg" />
<span>Discount code</span>
{{ discounts }}
</div>
<div class="discount-container">
<div v-for="(c,idx) in discounts" class="discounts" :key="idx">
<div class="perc-input">
<input style="max-width: 50px;" v-model.number="c.discount" type="number" min="1" max="100" step="1" placeholder="10">
<div>%</div>
</div>
<input class="code-input" v-model="c.code" placeholder="Code">
<img src="~/assets/svgs/bin.svg" title="Delete code" #click="deleteCode(idx)" v-if="discounts.length > 1"/>
</div>
</div>
<span #click="newDiscount" class="add-another">+ Add another discount</span>
</div>
</template>
<script>
export default {
props: {
value: {
type: Array,
},
tid: { type: Number, default: 0 },
},
data() {
return {
discounts: this.value,
}
},
mounted() {
if (this.discounts.length == 0) {
this.newDiscount();
}
},
methods: {
newDiscount() {
this.$store.commit('ticketDiscount/addDiscount',
{
"id": this.tid,
"name": "discount",
"value": { code: null,discount: null }
});
},
deleteCode(index) {
this.discounts.splice(index, 1);
}
},
watch: {
discounts() {
this.$emit('input', this.discounts)
}
},
beforeDestroy() {
this.$emit('input', []);
}
}
</script>
you shouldn't use v-model in this case.
<input style="max-width: 50px;" v-model.number="c.discount" .../>
you could just set the value
<input style="max-width: 50px;" :value="c.discount" #change="handleValueChange" .../>
and then in handleValueChange function to commit the action to update just for that value.

Vue-Router doesn't work in a Select Option

I I try to create a simple navigation from a select option but without results.
<router-link> does not work inside <select><option>
Here's what I tried :
vue
<div>
<select v-model="selected">
<option v-for="orga in organisations" :key="orga.name">
<router-link :to="{name: 'Box', params: {orga: orga.route}}">
{{ orga.name }}
</router-link>
</option>
</select>
</div>
javascript
data() {
return {
selected: this.$route.params.orga,
organisations: [
{ name: "Abc", route: "abc" },
{ name: "Lmn", route: "lmn" },
{ name: "Xyz", route: "xyz" }
]
};
}
If you want to change your view based on the select option, you can't use a router-link inside an option tag.
However, you can achieve this by a workaround shown below. Here we will be switching the views based on the select option and changing the route.
Vue.component('compA', {
template: '<div>{{name}}</div>',
data() {
return {
name: 'Component A'
}
}
})
Vue.component('compB', {
template: '<div>{{name}}</div>',
data() {
return {
name: 'Component B'
}
}
})
const routes = [{
path: '/a',
component: {
template: '<compA/>'
}
},
{
path: '/b',
component: {
template: '<compB/>'
}
}
];
const router = new VueRouter({
routes
})
new Vue({
el: "#app",
data() {
return {
selected: ''
}
},
router,
methods: {
routeChange: function(e) {
this.$router.push(e.target.value)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<div>
<select #change="routeChange">
<option></option>
<option v-for="(c, i) in ['a', 'b']" :key="i">
{{ c }}
</option>
</select>
</div>
<router-view/>
</div>
But this scenario can be alternatively achieved by dynamic components. The docs explains more about this which can be used to switch between components or dynamic render.
Vue.component('CompA', {
template: '<div>new component A</div>'
})
Vue.component('CompB', {
template: '<div>new component B</div>'
})
new Vue({
el: "#app",
data() {
return {
value: ""
}
},
template: `
<div>
<select v-model="value">
<option v-for="c in ['compA', 'compB']">{{c}}</option>
</select>
<component :is="value" />
</div>
`
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<div id="app"></div>

Can a Vue component behave like wordpress shortcode attributes

Can a Vue component behave like wordpress shortcode attributes? For example in [wp-shortcode post_type="post"], I can define the post type in the shortcode and the data will be fetched accordingly.
Can something similar be done in Vue? For example <grid-one type="posts" /> or <grid-one type="videos" />. The type in the grid-one tag will change the request.type in the data.
GridOne.vue
<template>
<main class="site-content">
<div class="container">
<section v-if="posts.length" class="articles-list">
<post-item
v-for="post in posts"
:key="post.id"
:post="post"
/>
<pagination
v-if="totalPages > 1"
:total="totalPages"
:current="page"
/>
</section>
</div>
</main>
</template>
<script>
import PostItem from '#/components/template-parts/PostItem'
import Pagination from '#/components/template-parts/Pagination'
export default {
name: 'GridOne',
components: {
PostItem,
Pagination
},
props: {
page: {
type: Number,
required: true
}
},
data() {
return {
request: {
type: 'posts',
params: {
per_page: this.$store.state.site.posts_per_page,
page: this.page
},
showLoading: true
},
totalPages: 0
}
},
computed: {
posts() {
return this.$store.getters.requestedItems(this.request)
}
},
methods: {
getPosts() {
return this.$store.dispatch('getItems', this.request)
},
setTotalPages() {
this.totalPages = this.$store.getters.totalPages(this.request)
}
},
created() {
this.getPosts().then(() => this.setTotalPages())
}
}
</script>

Vue.js and Multiselect - The name 'input' does not exist in the current context

I'm trying to start a method after the value of the component Vue Multiselect has been changed using #input, but I'm getting the following compilation error:
CS0103: The name 'input' does not exist in the current context
Here's my multiselect:
<multiselect v-model="Instalacao.value" label="Serie" track-by="Serie" placeholder="Nº de série" :options="Instalacoes"
:multiple="false" :searchable="true" :allow-empty="false" :disabled="Editando" #input="getTecnicosByRepresentante">
<span slot="noResult">Nenhum técnico encontrado</span>
</multiselect>
This example works as expected: both the watch and the #input handler execute when you select a value. Your problem is probably not in the code that you included here.
new Vue({
el: '#app',
components: {
Multiselect: window.VueMultiselect.default
},
data: {
Instalacao: {
value: null
},
Instalacoes: [{
Serie: 'one',
value: 'Vue.js'
},
{
Serie: 'two',
value: 'Vue-Multiselect'
},
{
Serie: 'three',
value: 'Vuelidate'
}
]
},
watch: {
'Instalacao.value': function(newValue) {
console.log('Updated', newValue);
}
},
methods: {
getTecnicosByRepresentante() {
console.log("Input detected, too");
}
}
})
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<link href="https://unpkg.com/vue-multiselect#latest/dist/vue-multiselect.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue-multiselect#latest/dist/vue-multiselect.min.js"></script>
<div id="app">
<multiselect v-model="Instalacao.value" label="Serie" track-by="Serie" placeholder="Nº de série" :options="Instalacoes" :multiple="false" :searchable="true" :allow-empty="false" #input="getTecnicosByRepresentante">
<span slot="noResult">Nenhum técnico encontrado</span>
</multiselect>
<pre>{{ Instalacao.value }}</pre>
</div>