VueJs: Form handling with Vuex and inputs generated with an API - vue.js

Here's an example of a component:
<script>
export default {
name: 'my-form',
computed: {
myModules() {
return this.$store.state.myModules;
}
}
</script>
<template>
<form>
<p v-for="module in myModules">
<input type="checkbox" :value="module.id" />
<label>module.name</label>
</p>
<button type="submit">Submit</button>
</form>
</template>
The associated store:
state: {
myModules: []
},
mutations: {
setModules(state, modules) {
state.myModules = modules;
}
},
actions: {
getModules({commit}) {
return axios.get('modules')
.then((response) => {
commit('setModules', response.data.modules);
});
}
}
And finally, an example of return of the API "getModules":
modules : [
{
id: 1,
name: 'Module 1',
isActive: false
},
{
id: 2,
name: 'Module 2',
isActive: false
},
{
id: 3,
name: 'Module 3',
isActive: false
}
]
My question: what's the best way to change the "isActive" property of each module to "true" when I check the checkbox corresponding to the associated module, directly in the store?
I know that Vuex's documentation recommends to use "Two-way Computed Property" to manage the forms, but here I don't know the number of modules that the API can potentially return, and I don't know their name.
Thank you in advance!

This is a little bit wicked approach, but it works. You can create an accessor object for every item you access in a loop:
const store = new Vuex.Store({
mutations: {
setActive (state, {index, value}) {
state.modules[index].isActive = value
}
},
state: {
modules : [
{
id: 1,
name: 'Module 1',
isActive: false
},
{
id: 2,
name: 'Module 2',
isActive: false
},
{
id: 3,
name: 'Module 3',
isActive: false
}
]
}
});
const app = new Vue({
el: '#target',
store,
methods: {
model (id) {
const store = this.$store;
// here i return an object with value property that is bound to
// specific module and - thanks to Vue - retains reactivity
return Object.defineProperty({}, 'value', {
get () {
return store.state.modules[id].isActive
},
set (value) {
store.commit('setActive', {index: id, value});
}
});
}
}
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.min.js"></script>
<div id="target">
<div v-for="(item, id) in $store.state.modules">
Module #{{ item.id }} state: {{ item.isActive }}
</div>
<div v-for="(item, id) in $store.state.modules">
<label>
Module #{{ item.id }}
<input type="checkbox" v-model="model(id).value"/>
</label>
</div>
</div>
This is still quite a messy approach, but at least you don't have to commit mutations directly in template. With a little help of Vue.set() you can use this approach even to overcome standard reactivity caveats.

I have an alternative solution for you. You could make a child component for the checkboxes to clean up the code a bit.
UPD: I just realised that everything that I and #etki proposed is an overkill. I left the old version of my code below in case you still want to take a look. Here is a new one:
const modules = [{
id: 1,
name: 'Module 1',
isActive: true,
},
{
id: 2,
name: 'Module 2',
isActive: false,
},
{
id: 3,
name: 'Module 3',
isActive: false,
},
];
const store = new Vuex.Store({
state: {
myModules: [],
},
mutations: {
SET_MODULES(state, modules) {
state.myModules = modules;
},
TOGGLE_MODULE(state, id) {
state.myModules.some((el) => {
if (el.id === id) {
el.isActive = !el.isActive;
return true;
}
})
}
},
actions: {
getModules({
commit
}) {
return new Promise((fulfill) => {
setTimeout(() => {
commit('SET_MODULES', modules);
fulfill(modules);
}, 500)
});
}
}
});
const app = new Vue({
el: "#app",
store,
data: {},
methods: {
toggle(id) {
console.log(id);
this.$store.commit('TOGGLE_MODULE', id);
}
},
computed: {
myModules() {
return this.$store.state.myModules;
},
output() {
return JSON.stringify(this.myModules, null, 2);
},
},
mounted() {
this.$store.dispatch('getModules').then(() => console.log(this.myModules));
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<script src="https://unpkg.com/vue"></script>
<div id="app">
<form>
<div v-for="data in myModules">
<label :for="data.id">{{ data.name }}: {{data.isActive}}</label>
<input type="checkbox" :id="data.id" :name="'checkbox-' + data.id" :checked="data.isActive" #change="toggle(data.id)">
</div>
</form>
<h3>Vuex state:</h3>
<pre v-text="output"></pre>
</div>
As you can see above you could just call a function on input change and pass an id as a parameter to a method that fires vuex action.
The old version of my code.
A new one on jsfiddle

Related

Default props value are not selected in vue3 options api

I created a select2 wrapper in vue3 with options API everything working fine but the problem is that when getting values from calling API it's not selected the default value in the select2 option. but when I created a static array of objects it does. I don't know why it's working when it comes from the API
Parent Component
Here you can I passed the static options array in options props and my selected value is 2 and it's selected in my Select2 component, but when passed formattedCompanies it's not which is the same format as the static options array then why is not selected any reason here..?
<template>
<Form #submitted="store()" :processing="submitting">
<div class="row">
<div class="col-lg-6">
<div class="form-group">
<label>Company Name</label>
<Select2
:options="options"
v-model="selected"
placeholder="Select Company"
/>
<ValidationError :errors="errors" error-key="name" />
</div>
</div>
</div>
</Form>
</template>
<script>
import Form from "#/components/Common/Form";
import Select2 from "#/components/Common/Select2";
export default {
components: {
Select2,
Form
},
data() {
return {
selected : 2,
companies : [],
options: [ // static array
{ id: 1, text: 'hello' },
{ id: 2, text: 'hello2' },
{ id: 3, text: 'hello3' },
{ id: 4, text: 'hello4' },
{ id: 5, text: 'hello5' },
],
}
},
mounted() {
this.getAllMedicineCompanies()
},
computed:{
formattedCompanies() {
let arr = [];
this.companies.forEach(item => {
arr.push({id: item.id, text: item.name})
});
return arr;
}
},
methods: {
getAllMedicineCompanies(){
axios.get('/api/get-data?provider=companies')
.then(({ data }) => {
this.companies = data
})
},
}
}
</script>
Select2 Component
Here is what my select2 component look like, did I do anything wrong here, please anybody help me
<template>
<select class="form-control">
<slot/>
</select>
</template>
<script>
export default {
name: "Select2",
props: {
options: {
type: [Array, Object],
required: true
},
modelValue: [String, Number],
placeholder: {
type: String,
default: "Search"
},
allowClear: {
type: Boolean,
default: true
},
},
mounted() {
const vm = this;
$(this.$el)
.select2({ // init select2
data: this.options,
placeholder: this.placeholder,
allowClear: this.allowClear
})
.val(this.modelValue)
.trigger("change")
.on("change", function () { // emit event on change.
vm.$emit("update:modelValue", this.value);
});
},
watch: {
modelValue(value) { // update value
$(this.$el)
.val(value)
.trigger("change");
},
options(options) { // update options
$(this.$el)
.empty()
.select2({data: options});
},
},
destroyed() {
$(this.$el)
.off()
.select2("destroy");
}
}
</script>
Probably when this Select2 mounted there is no companies. It is empty array after that it will make API call and it it populates options field and clear all options.
Make:
companies : null,
Change it to
<Select2
v-if="formattedCompanies"
:options="formattedCompanies"
v-model="selected"
placeholder="Select Company"
/>
It should be like this:
<template>
<Form #submitted="store()" :processing="submitting">
<div class="row">
<div class="col-lg-6">
<div class="form-group">
<label>Company Name</label>
<Select2
v-if="formattedCompanies"
:options="formattedCompanies"
v-model="selected"
placeholder="Select Company"
/>
<ValidationError :errors="errors" error-key="name" />
</div>
</div>
</div>
</Form>
</template>
<script>
import Form from "#/components/Common/Form";
import Select2 from "#/components/Common/Select2";
export default {
components: {
Select2,
Form
},
data() {
return {
selected : 2,
companies : null,
options: [ // static array
{ id: 1, text: 'hello' },
{ id: 2, text: 'hello2' },
{ id: 3, text: 'hello3' },
{ id: 4, text: 'hello4' },
{ id: 5, text: 'hello5' },
],
}
},
mounted() {
this.getAllMedicineCompanies()
},
computed:{
formattedCompanies() {
let arr = [];
this.companies.forEach(item => {
arr.push({id: item.id, text: item.name})
});
return arr;
}
},
methods: {
getAllMedicineCompanies(){
axios.get('/api/get-data?provider=companies')
.then(({ data }) => {
this.companies = data
})
},
}
}
</script>
The problem was that my parent component and Select2 component mounted at the same time that's why my computed value is not initialized so the selected value is not selected in the option,
problem solved by setTimeOut function in mounted like this
Select2 Component
<script>
mounted() {
const vm = this;
setTimeout(() => {
$(this.$el)
.select2({ // init select2
data: this.options,
placeholder: this.placeholder,
allowClear: this.allowClear
})
.val(this.modelValue)
.trigger("change")
.on("change", function () { // emit event on change.
vm.$emit("update:modelValue", this.value);
});
}, 500)
},
</script>

How to pass argument to $store.getters with onclick event?

How to pass argument to $store.getters with onclick event? I can see the default value but not the new value. This is my code
const store = new Vuex.Store({
state: {
notes: [
{ id: 1, text: 'Hello World 1, Vuex!', deleted: false},
{ id: 2, text: 'Hello World 2, Vuex!', deleted: true},
{ id: 3, text: 'Hello World 3, Vuex!', deleted: false}
]
},
getters: {
getNoteById: (state) => (id) => {
return state.notes.find(note => note.id === id);
}
}
});
new Vue({
el: "#app",
data() {
return {
result: ''
}
},
store,
computed: {
note() {
return this.$store.getters.getNoteById(3);
}
},
methods: {
showText(id) {
this.result = this.$store.getters.getNoteById(id);
}
}
});
and html
<div id='app'>
<p>Default value: {{ note.text }}</p>
<button #click="showText('1')">1</button>
<button #click="showText('2')">2</button>
<button #click="showText('3')">3</button>
<p>New value: {{ result.text }}</p>
</div>
Your getter is working perfectly. And it correctly returns undefined when searching for any item with an id of '1', '2' or '3'.
Because the store doesn't contain any such item.
All your store items have number ids, none of them has a string id. See it working (I changed the template to send numbers):
Vue.config.productionTip = false;
Vue.config.devtools = false;
const store = new Vuex.Store({
state: {
notes: [
{ id: 1, text: 'Hello World 1, Vuex!', deleted: false},
{ id: 2, text: 'Hello World 2, Vuex!', deleted: true},
{ id: 3, text: 'Hello World 3, Vuex!', deleted: false}
]
},
getters: {
getNoteById: (state) => (id) => {
return state.notes.find(note => note.id === id);
}
}
});
new Vue({
el: "#app",
data() {
return {
result: ''
}
},
store,
computed: {
note() {
return this.$store.getters.getNoteById(3);
}
},
methods: {
showText(id) {
this.result = this.$store.getters.getNoteById(id);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id='app'>
<p>Default value: {{ note.text }}</p>
<button #click="showText(1)">1</button>
<button #click="showText(2)">2</button>
<button #click="showText(3)">3</button>
<p>New value: {{ result.text }}</p>
</div>
If, for any reason, you don't have control over the incoming type (you might have this problem when reading the value of an <input>, which is always a string, even when the type of the input is "number"), you might want to cast the value as number: +'5' => 5.
So, as an alternative to fixing the template, as above, you could do this in the method:
showText(id) {
this.result = this.$store.getters.getNoteById(+id);
}

Using a method as data inside the app parent

So I'm still learning Vue.js and I got my list working well and I have one question. I will explain what I'm trying to do below as best as possible and I wanted to see if someone could help me with my issue.
So here is the component that I have on the HTML side:
<favorites-edit-component
v-for="(favorite, index) in favorites"
v-bind:index="index"
v-bind:name="favorite.name"
v-bind:key="favorite.id"
v-on:remove="favorites.splice(index, 1)"
></favorites-edit-component>
Here is the vue.js portion that I have:
Vue.component('favorites-edit-component', {
template: `
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ name }}</span>
<span class="icon is-small favorite-delete" v-on:click="$emit('remove')">
<i class="fas fa-times"></i>
</span>
</button>
</div>
`,
props: ['name'],
});
new Vue({
el: '#favorites-modal-edit',
data: {
new_favorite_input: '',
favorites: [
{
id: 1,
name: 'Horse',
url: 'www.example.com',
},
{
id: 2,
name: 'Sheep',
url: 'www.example2.com',
},
{
id: 3,
name: 'Octopus',
url: 'www.example2.com',
},
{
id: 4,
name: 'Deer',
url: 'www.example2.com',
},
{
id: 5,
name: 'Hamster',
url: 'www.example2.com',
},
],
next_favorite_id: 6,
},
methods: {
add_new_favorite: function() {
this.favorites.push({
id: this.next_favorite_id++,
name: this.new_favorite_input
})
this.new_favorite_input = ''
},
get_favorite_menu_items: function() {
wp.api.loadPromise.done(function () {
const menus = wp.api.collections.Posts.extend({
url: wpApiSettings.root + 'menus/v1/locations/favorites_launcher',
})
const Menus = new menus();
Menus.fetch().then(posts => {
console.log(posts.items);
return posts.items;
});
})
}
}
});
So as you can see, I have the data: { favorites: [{}] } called inside the vue app and I get this console.log:
Now I built a method called get_favorite_menu_item and this is the return posts.items output inside console.log:
Problem: I don't want to have a manual array of items, I want to be able to pull in the method output and structure that - How would I take a approach on pulling the items?
Could I call something like this:
favorites: this.get_favorite_menu_items?
Here is a JFiddle with all the items: https://jsfiddle.net/5opygkxw/
All help will be appreciated on how to pull in the data.
First I will init favorites to empty array.
then on get_favorite_menu_items() after I will init data from post.item to favorites.
on created() hooks i will call get_favorite_menu_items() to fetch the data when the view is created.
new Vue({
el: '#favorites-modal-edit',
data: {
new_favorite_input: '',
favorites: [],
next_favorite_id: 6,
},
methods: {
add_new_favorite: function() {
this.favorites.push({
id: this.next_favorite_id++,
name: this.new_favorite_input
})
this.new_favorite_input = ''
},
get_favorite_menu_items: function() {
wp.api.loadPromise.done(function () {
const menus = wp.api.collections.Posts.extend({
url: wpApiSettings.root + 'menus/v1/locations/favorites_launcher',
})
const Menus = new menus();
Menus.fetch().then(posts => {
console.log(posts.items);
// need map key also
this.favorites = posts.items;
});
})
}
},
created () {
// fetch the data when the view is created
this.get_favorite_menu_items();
},
});

vuejs reactivity of complex objects

I am using Vue.js 2.5.17 I have two components, App (parent) and TreeNode (child), which display a tree structure of items from a deeply nested object. Each node in the tree is presented with the TreeNode component which is a recursive component.
TreeNode component
const TreeNode = Vue.component('TreeNode', {
name: 'TreeNode',
template: '#treenode',
props: {
model: Object,
},
data() {
return {
open: false,
};
},
computed: {
isExpandable() {
return this.model.children &&
this.model.children.length;
},
},
methods: {
toggle() {
if (this.isExpandable) {
this.open = !this.open;
}
},
},
});
TreeNode template
<script type="text/x-template" id="treenode">
<li>
<input type="checkbox" :id="model.name" style="display:none;"/>
<label :for="model.name" style="color:gray;" #click="toggle">
{{ model.name }}
{{ model.data.example }}
</label>
<ul v-show="open" v-if="isExpandable">
<TreeNode
class="item"
v-for="(model, index) in model.children"
:key="index"
:model="model">
</TreeNode>
</ul>
</li>
</script>
App component template
<script type="text/x-template" id="oulist">
<div>
<div id="unitsTable" class="row filterlist treelist b_widget2" style="width:85%;">
<div class="css-treeview">
<TreeNode
class="item"
:model="items">
</TreeNode>
</div>
</div>
</script>
App component
const App = Vue.component('App', {
template: '#oulist',
components: {
TreeNode,
},
data() {
return {
items: {
name: 'item1',
data: { example: '1' },
children: [
{
name: 'item11',
children: [],
data: { example: '1' },
},
{
name: 'item12',
children: [
{ name: 'item121', children: [], data: { example: '1' } },
{ name: 'item122', children: [], data: { example: '1' } },
],
data: { example: '1' },
},
],
},
};
},
methods: {
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
},
});
The object posted above is just an example, my actual object has a lot more nested levels and items per level.
The problem am I facing is that whenever a property deep within my items object is changed (more specifically, the example property of the data object inside a node), the DOM is not updated. I read through the reactivity caveats and saw that adding new properties is not reactive by default, but I am not adding new properties here, just changing the existing ones.
When data from a tree node is updated, I traverse the object to find the correct node and update its properties as follows:
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
this.updateItem(this.items, 'item121', { example: 'newVal' });
Any tips ? Thanks!
EDIT: The data is always changed only in the parent (App) component.

Why is the computed property of this component not reactive:

I have an object like this:
myObject: {
items: [{
title: '140',
isActive: true,
}, {
title: '7',
isActive: false
}, {
title: '10',
isActive: false
}]
}
Which I'm using like this:
<my-component :items="myObject.items"></my-component>
This is how the component looks like:
<template>
<div class="i-panel panel-container d-flex"
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->
</div>
</div>
</template>
<script>
export default {
name: 'IPanel',
props: {
items: {
type: Array,
default () {
return []
}
}
},
computed: {
// code
prefs () {
return {
items: this.items
}
}
},
methods: {
onClick (item) {
this.prefs.items.forEach(item => {
if (JSON.stringify(item) === JSON.stringify(clickedItem)) {
item.isActive = true
}
})
}
}
}
</script>
When I click an item (and that item is the same as the clickedItem), it's supposed to become isActive true. It does. But I have to refresh the Vue devtools or re-render the page for the change to take effect.
Why isn't item.isActive = true reactive?
In the code you posted, you are using a clickedItem object that is not defined anywhere. I don't know if this is just in the process of writing your question or if it is your problem.
However, when using clickedItem the right way, it seems to work: https://jsfiddle.net/d5z93ygy/4/
HTML
<div id="app" class="i-panel panel-container d-flex">
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->{{ item.isActive ? 'active' : 'inactive' }}
</div>
</div>
JS
new Vue({
el: "#app",
data: {
items: [{
title: '140',
isActive: true,
}, {
title: '7',
isActive: false
}, {
title: '10',
isActive: false
}]
},
computed: {
// code
prefs () {
return {
items: this.items
}
}
},
methods: {
onClick (clickedItem) {
this.prefs.items.forEach(item => {
if (JSON.stringify(item) === JSON.stringify(clickedItem)) {
item.isActive = true
}
})
}
}
})
Change
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->
</div>
to
<div
v-for="(item, index) in prefs.items"
class="panel-item"
#click="onClick(index)">
<!-- some content -->
</div>
Then, in your change method, go like this:
onClick (index) {
Vue.set(this.items, index, true);
}
https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats