How to bind checkboxes to Vuex store? - checkbox

I have a component that contains some checkboxes. I need to be able to access which checkboxes are checked from other components in my Vue application, but I cannot for the life of me figure out (nor find online) how to properly connect the checkboxes to my Vuex store.
What is the right way to connect checkboxes within a component to a Vuex store, so that they act just as if the checkbox was connected to the components data via v-model?
Here is a starting point for what I'm trying to do (in a very very basic sense)
https://jsfiddle.net/9fpuctnL/
<div id="colour-selection">
<colour-checkboxes></colour-checkboxes>
</div>
<template id="colour-checkboxes-template">
<div class="colours">
<label>
<input type="checkbox" value="green" v-model="colours"> Green
</label>
<label>
<input type="checkbox" value="red" v-model="colours"> Red
</label>
<label>
<input type="checkbox" value="blue" v-model="colours"> Blue
</label>
<label>
<input type="checkbox" value="purple" v-model="colours"> Purple
</label>
<chosen-colours></chosen-colours>
</div>
</template>
<template id="chosen-colours-template">
<div class="selected-colours">
{{ colours }}
</div>
</template>
const store = new Vuex.Store({
state: {
colours: []
}
});
Vue.component('colour-checkboxes', {
template: "#colour-checkboxes-template",
data: function() {
return {
colours: []
}
}
});
Vue.component('chosen-colours', {
template: "#chosen-colours-template",
computed: {
colours() {
return store.state.colours
}
}
});
const KeepTalkingSolver = new Vue({
el: "#colour-selection"
});
The aim is to get the colours that are selected in the colour-checkboxes component to output in the chosen-colours component, going through the Vuex store.

You can use computed property with getter as vuex getter and setter in computed property which will call a mutation for that state property to do this.
You can see an example of this here with two-way Computed Property:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}

I wanted to provide an answer that actually uses checkboxes.
There is one possible solution outlined here:
Vuex dynamic checkboxes binding
And a simpler solution can be achieved something like the following:
<div v-for="tenant in tenants"
v-bind:key="tenant.id"
class="form-group form-check">
<input type="checkbox"
class="form-check-input"
v-bind:id="tenant.id"
v-bind:value="tenant.name"
#change="updateSelectedTenants">
Key here is calling a method using on-change, it will pass an event to the method with all the details needed to make the change.
The #change function:
updateSelectedTenants(e) {
console.log('e', e.target)
console.log('e', e.target.value)
this.$store.dispatch('updateSelectedTenants', e.target)
}
Here I want the value, in this case will be the tenants name, but further inspection of the target also gives the 'id', and whether or not the checkbox is 'checked' or unchecked.
Over in the store, we can manipulate the 'selectedTenants' array:
updateSelectedTenants (context, tenant) {
if(tenant.checked) {
// Tenant checked, so we want to add this tenant to our list of 'selectedTenants'
context.commit('addSelectedTenant', { id: tenant.id, name: tenant.value })
} else {
// otherwise, remove the tenant from our list
context.commit('removeSelectedTenant', tenant.id)
}
}
Here are the actual mutators:
addSelectedTenant (state, tenant) {
this.state.selectedTenants.push(tenant)
},
removeSelectedTenant (state, id) {
this.state.selectedTenants = this.state.selectedTenants.filter(tenant => {
return tenant.id != id
})
The vuejs docs are great, but sometimes they can be a little light on with real world examples. I don't think it's possible to achieve the above using a computed value, with get(), set()... but I'd like to see a solution that can.

OK I have been challenged to show my solution. Here it is on jsfiddle
the html is:
<div id="app">
<label v-for="brother in ['Harpo','Groucho','Beppo']">
<input type='checkbox' v-model='theBrothers' v-bind:value='brother' />
{{ brother }}
</label>
<div>
You have checked: {{ theBrothers }}
</div>
</div>
and the js is:
const store = new Vuex.Store({
state: {
theBrothers: []
},
})
new Vue({
el: "#app",
store: store,
computed: {
theBrothers: {
set(val){this.$store.state.theBrothers = val},
get(){ return this.$store.state.theBrothers }
}
},
})

2021 - easy, readable, & taking advantage of the power of Vue/Vuex...
There are lots of complicated answers for a simple problem. Run the snippet below to see it in action.
Here is a working solution that solves all of the issues described below:
const store = new Vuex.Store({
state: {
names: ['Max'],
},
mutations: {
setNames(state, names) {
state.names = names;
}
}
});
new Vue({
el: '#app',
store,
computed: {
selectedNames: {
get: function() {
return this.$store.state.names;
},
set: function(val) {
console.log(val);
this.$store.commit('setNames', val);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuex#3.6.2/dist/vuex.js"></script>
<div id="app">
<div>
<input type="checkbox" v-model="selectedNames" :value="'John'" id="checkbox-1" />
<label for="checkbox-1">Click me to add my value to the state</label>
<br />
<input type="checkbox" v-model="selectedNames" :value="'Max'" id="checkbox-2" />
<label for="checkbox-2">I am preselected since my value already exists in the state <code>names</code> array</label>
<div>State: <strong>{{ names }}</strong></div>
</div>
</div>
Long story short all you need to do is take a piece of state (names below), create a mutation (setNames below) to set it, and then bind the v-model to a computed (selectedNames below) that has a getter and setter, the getter gets the piece of state names, and the setter calls the mutation setNames.
In my opinion this is the cleanest solution to this problem because it follows the natural pattern of Vue/Vuex and how checkboxes are typically implemented.
Other answers in here attempt to mutate the state directly without mutations, while some other answers avoid using a v-model which presents issues with having a preselected value and requires much more code, and finally the accepted answer doesn't even show any HTML template code on how to implement it.

Use #change to update Vuex as needed:
HTML:
<input
v-for='item in items'
#change='update_checkboxes'
v-model='selected_checkboxes'
:value='item.id'
type='checkbox
/>
<label>{{item.name}}</label>
JS:
data: function(){
return {
selected_checkboxes: [] // or set initial state from Vuex with a prop passed in
}
},
methods: {
update_checkboxes: function(){
this.$store.commit('update_checkboxes', this.selected_checkboxes)
}
}

Based on the solution from #Saurabh - it is important to use actions and getters rather than directly accessing vuex state - this will ensure consistency throughout the application.
<p>Mega test: <input type="checkbox" v-model="mega" /></p>
computed: {
mega: {
get () {
return this.$store.getters.mega
},
set (value) {
this.$store.dispatch('updateMega', value)
}
}
}
const store = new Vuex.Store({
state: {
mega: false
},
getters: {
mega (state) {
return state.mega
}
},
mutations: {
updateMega (state, value) {
state.mega = value
}
},
actions: {
updateMega (context, value) {
context.commit('updateMega', value)
}
}
})

You shoud remove colours = [] in data.

Related

Can't filter aync data on mounted vuejs

I'm working on a vuejs/(vuex) for state management/firebase project of posts.
So I have a firestore collection of posts (array of objects that have name id owner content and timestamp for creation ...)
I'm retrieving that data by using the onSnapshot methode and it's stored on blogPosts variable... and we show theme all, when a user want to visit a single post it will redirect him to the route of the single post (..../view-post/postid) and i filter that array using the id of the post to have an array of one element (which is the post he visited)
when the filter complete i got all the data and i fill theme on the template like these
<template>
<NavBarView />
<section class="preview" v-if="currentBlog">
<h2>{{ currentBlog[0].blogTitle }}</h2>
<p>Posted on: <span>{{ dateFormat() }}</span> <span v-if="currentBlog[0].editTime">(Last edit:
{{ editFormat() }})</span></p>
<img class="blogCover" :src="currentBlog[0].blogCoverFileURL" :alt="currentBlog[0].blogCoverPhoto">
<div class="html-blog" v-html="currentBlog[0].blogHTML"></div>
<hr />
</section>
</template>
<script>
export default {
data() {
return {
currentBlog: null
}
},
mounted: {
this.currentBlog = this.blogPosts.filter((post) => {
return post.blogID == this.$route.params.id
})
},
computed: {
...mapState(['blogPosts'])
}
//note that i have exported all the requirements such as mapState and firebase functions ... didn't write theme here
}
</script>
now the problem is that the filter is occurring before the data are fetched from firebase and i can't handle that so it's always returning can't read property of null (currentBlog[0])
i found a solution which is sending a getDoc request to firebase but it's a bad idea, (why i send request and i have all the posts here so i can filter it directly and get the specific post) which didn't work!!!!!
any solution??
Looking at your template, I'm under the impression currentBlog should not be an array, as you don't ever have more than 1 element in that array (you're filtering by blogID, which I'm guessing is a unique identifier). You need to .find() the blog entry, not .filter() it:
computed: {
blogID() {
return this.$route.params.id;
},
currentBlog() {
return this.$store.state.blogPosts.find((b) => b.blogID === this.blogID);
}
}
Note: if state.blogPosts does not start as an empty array (as it should), you might want to use:
currentBlog() {
return (this.$store.state.blogPosts || []).find(
(b) => b.blogID === this.blogID
);
}
And now replace all currentBlog[0]s with currentBlog:
<template>
<NavBarView />
<section class="preview" v-if="currentBlog">
<h2>{{ currentBlog.blogTitle }}</h2>
<p>
Posted on: <span>{{ dateFormat() }}</span>
<span v-if="currentBlog.editTime">(Last edit: {{ editFormat() }})</span>
</p>
<img
class="blogCover"
:src="currentBlog.blogCoverFileURL"
:alt="currentBlog.blogCoverPhoto"
/>
<div class="html-blog" v-html="currentBlog.blogHTML"></div>
<hr />
</section>
</template>
Important: make sure in every single method/computed/watch in the script part of the component, if using currentBlog, you're first checking if it's not falsy. Generic example:
computed: {
blogHTML() {
return this.currentBlog?.blogHTML
/* shorthand for:
* return this.currentBlog && currentBlog.blogHTML
*/
}
}
I would suggest that rather than trying to do this operation once on mounted that you instead re-run the filter every time that state.blogPosts is updated. You can easily do this through a computed property. See below:
export default {
data() {
return {
}
},
computed: {
...mapState(['blogPosts']),
currentBlog(){
const posts = this.$store.state.blogPosts;
if(Array.isArray(posts)) {
return posts.filter((post) => {
return post.blogID == this.$route.params.id
});
} else {
return null
}
}
}
}

Vue input tag element - how to fill it with tags properly?

I am using vue element <input-tag>, and I need to make edit page for one entity which contains tags. So the <input-tag> field should be populated based on the current values ​​in the entity.
I managed to do that, but the whole JSON object appears in <input-tag> field (example: {"id":2, "tagName": "vuejsproblem", "newsId": 1}), not only tagName as it should be.
Also, in console I got error, I don't know why, because this.tags is obviously not "":
[Vue warn]: Invalid prop: type check failed for prop "value". Expected Array, got String with value "".
found in
---> <InputTag>
This is code of my vue page:
<template>
<div class="pt-5">
<form #submit.prevent="editNews">
<div id="app">
<label for="tags">Tags</label>
<input-tag placeholder="Add Tag" v-model.trim="tags"></input-tag>
<br>
</div>
<button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>
</div>
</template>
<script>
export default {
name: "EditNews",
data() {
return {
tags: '',
}
},
beforeMount () {
this.$axios.get(`/api/news/${this.$route.params.id}`,{
id: this.$route.params.id,
}).then((response) => {
this.tags = response.data.tags;
});
/* this.tags.forEach(element => element.tagName);
*/
},
methods: {
editNews() {
this.message = '';
if(this.tags.length > 1) {
console.log(this.tags)
this.$axios.post(`/api/news/${this.$route.params.id}`, {
tags: this.tags,
}).then(
data => {
this.message = data.message;
},
error => {
this.message = error.response.data
alert(this.message);
});
}
},
},
}
</script>
<style scoped>
</style>
As it is said here, tags must be Array. So,
define tags as Array in data() like:
data() {
return {
tags: [],
}
}
Also response.data.tags must be Array something like [ "Jerry", "Kramer", "Elaine", "George" ] as here.
You can convert response.data.tags to Array which contains only tagName by doing this: this.tags = response.data.tags.map(x => x.tagName)
Based on what I could understand from your question (if I got it right), you can do that using the render helper from vuejs, depending on which version you're trying to achieve this v2 or v3
Back in v2 you could do something like:
https://github.com/vubular/elements/blob/master/src/text/Plain.vue
using the this._v you can bind directly the content from the slot of the component or you could wrap with any html tag before passing the content in for example this._v('<span>'+this.content+'</span>'), you can also define tag as prop and then render prop instead of hardcoded tags.
Meanwhile in v3 you can return the h helper imported from vue:
render() { return h('span', {}, this.transformedContent); },. Again you can accept the span/tag as prop in child component context.
When doing so you don't need to define a <template> for your component so that you can render the component using vue helpers (where it then handles the DOM interactions).

How do I notify VueJS/Vuex of an value change in an array?

when I update a value in an array of the store, the interface doesn't reflect the change.
Goal: I'm trying to display a simple minesweeper grid. Once I click on a cell, the object attach to that cell should update isRevealed to true to say is was revealed and Vuejs should add the class dug to that cell.
What's working: isRevealed is switched to true.
What was working: using only props everything was working, but trying to learn VueJS and including Vuex store the UI doesn't update anymore with the array.
Structure:
App shows a grid and grid shows multiple cell.
store.js for Vuex
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
GameGrid: [],
},
mutations: {
revealCell(state, cell) {
state.GameGrid[cell.y][cell.x].isRevealed = true;
console.log("revealCell ", cell);
},
});
GameGrid contain the following:
10 arrays of 10 objects:
{
isRevealed: false,
x: 0,
y: 0
...
}
grid.vue, nothing special, display every cell
<template>
<div class="grid">
<div v-for="row in $store.state.GameGrid" class="row">
<app-cell v-for="col in row" :cell="col"></app-cell>
</div>
</div>
</template>
cell.vue:
<template>
<div
#click="dig"
:class="{dug: cell.isRevealed, dd: cell.debug}">
<span class="text" v-html="cell.display"></span>
<span class="small">{β€Œ{ cell.code }}</span>
</div>
</template>
<script>
export default {
props: {
cell: {
type: Object
},
},
data: function() {
return {
display: null,
}
},
methods: {
dig() {
this.$store.commit('revealCell', this.cell);
},
}
</script>
Edit accoridng to pirs answer:
Grid.vue file
<template>
<div class="grid">
<div v-for="row in gameGridAlias" class="row">
<app-cell v-for="col in row" :cell="col"></app-cell>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import Cell from './Cell.vue';
export default {
components: {
'app-cell': Cell,
},
data: {
gameGridAlias: []
},
methods: {
},
computed: mapState({
gameGridAlias: state => state.GameGrid
})
}
</script>
Note that I get The "data" option should be a function that returns a per-instance value in component definitions. error since data doesn't return a function.
My functions to set isRevealed are on the cell.vue though. I'm still seeing no UI updates (except when I'm saving?). Cells with this.cell.isRevealed = true confirmed with console.log but no change in classes.
Here is how I'm populating GameGrid
This function is calling when the user press a button on the hmm, home page? App.vue file.
generateGrid() {
console.log("generateGrid");
//Generate empty play grid
for(let y = 0; y < this.$store.state.GameGridHeight; y++) {
this.$store.state.GameGrid[y] = new Array();
for(let x = 0; x < this.$store.state.GameGridWidth; x++) {
this.$store.state.GameGrid[y][x] = {
content: 'Empty',
code: 0,
isRevealed: false,
x: x,
y: y,
debug: false,
};
}
}
}, //End generateGrid
In your grid.vue, you should use computed and mapState to update the props
It would be like:
import { mapState } from 'vuex'
// ...
<div v-for="row in gameGridAlias" class="row">
// ...
computed: mapState({
gameGridAlias: state => state.GameGrid
})
More : https://vuex.vuejs.org/en/state.html#the-mapstate-helper
*Note: mapState is optional, but you can handle multiple stores with it and it's robust, i use it for all cases personally.
Edit: Don't use this.$store.state.GameGrid[y] or another this.$store.state vars but only mapState aliases to make it work correctly
state.GameGrid[cell.y][cell.x].isRevealed = true;
add this
state.GameGrid = Array.from(state.GameGrid); //force reflection by cloning array
It is not the most efficient solution but it only one that works for me

Get element sibling value in React

I have this method inside a React component (which I later pass to the render() method):
renderNutrientInputs: function (num) {
var inputs = [];
for (var i =0; i < num; i++) {
inputs.push(<div key={i}>
<label>Nutrient name: </label><input type="text"/>
<label>Nutrient value: </label><input type="text" />
</div>);
}
return inputs;
}
I'm trying on each change of the "Nutrient value" textbox, to also grab the current value of the "Nutrient name" textbox. I first though of assigning "ref" to both of them, but I figured there might be multiple pairs of them on the page (and the only way to identify them would be by key). I also tried something like this:
<label>Nutrient name: </label><input type="text" ref="nutName"/>
<label>Nutrient value: </label><input type="text" onChange={this.handleNutrientValueChange.bind(null, ReactDOM.findDOMNode(this.refs.nutName))}/>
but got a warning from React:
Warning: AddForm is accessing getDOMNode or findDOMNode inside its
render(). render() should be a pure function of props and state. It
should never access something that requires stale data from the
previous render
Is there some way to attach onChange event listener to Nutrient value text box and access the current value of "Nutrient name" textbox in the event listener function?
You don't want to access DOM elements directly. There is no need to do so... Work with your data, forget about DOM!
What you want is to "listen to changes to n-th nutritient. I want to know it's name and it's value". You will need to store that data somewhere, let's say in state in this example.
Implement getInitialState method. Let's begin with empty array, let user to add nutritients.
getInitialState() {
return { nutritients: [] };
},
In render method, let user add nutrition by click on "+", let's say
addNutritient() {
const nutritients = this.state.nutritients.concat();
nutritients.push({ name: "", value: undefined });
this.setState({ nutritients });
},
render() {
return (
<div>
<div onClick={this.addNutritient}>+</div>
</div>
)
}
Okay, let's focus on rendering and updating nutritients:
addNutritient() {
const nutritients = this.state.nutritients.concat();
nutritients.push({ name: "", value: undefined });
this.setState({ nutritients });
},
renderNutritients() {
const linkNutritient = (idx, prop) => {
return {
value: this.state.nutritients[idx][prop],
requestChange: (value) {
const nutritients = this.state.nutritients.concat();
nutritients[idx][prop] = value;
this.setState({ nutritients });
},
}
};
const nutritients = [];
return (
this.state.nutritients.map((props, idx) => (
<div>
<input valueLink={linkNutritient(idx, "name")} />
<input valueLink={linkNutritient(idx, "value")} />
</div>
))
)
},
render() {
return (
<div>
{ this.renderNutritients() }
<div onClick={this.addNutritient}>+</div>
</div>
)
}
Coding by hand, sorry for syntax error or typings.
Edit:
Take a look at this working Fiddle https://jsfiddle.net/Lfrk2932/
Play with it, it will help you to understand what's going on.
Also, take a look at React docs, especialy "valueLink" https://facebook.github.io/react/docs/two-way-binding-helpers.html#reactlink-without-linkedstatemixin
I prefer not to use 2 way binding with React which is kind of a flux anti-pattern. Just add a onChange listener to your input element and setState.
Your state will be something like this:
{0: {nutrientName: xyz, nutrientValue: 123},
1: {nutrientName: abc, nutrientValue: 456}}
So when you change the nutrientvalue 456 to say 654, you can say its corresponding name is abc and vice versa.
The whole thing about React is about handling the data not the DOM :)

How to reset HTML input fields if I don't know beforehand how many inputs there will be?

Any ideas how to reset input fields when they are dynamically generated? Couldn't find any information about this. If I would know in advance how many fields there will be it would be easy with ref or just have unique value attributes on every input element and the call getInitialState().
https://jsfiddle.net/69z2wepo/15407/
Size of the product array can vary so how would you reset all of the input fields?
If you wrap your (uncontrolled) inputs in a HTML <form> element, there's always
<button type="reset">Reset</button>
Otherwise, you'll probably want to bind the input value to a prop or state item, and then use handleReset to retrieve and (re)set these values. When an item in state or prop changes, React will automatically update the affected parts of your DOM.
Read more about form inputs in the docs: http://facebook.github.io/react/docs/forms.html#controlled-components
you can set this.setState by array of values
https://jsfiddle.net/amb223fx/
Edit: As mgrim said, you can use HTML form reset. Or, if you need something more sophisticated, keep reading.
Take a look at this:
var products = [
{name: 'Chocolate'},
{name: 'Chips'},
{name: 'Candy'}
];
// Size of the products array can vary!
var Hello = React.createClass({
handleReset: function(){
var products = this.state.products.map(function (product) {
return Object.assign({}, product, {
value: 0
});
});
this.setState({products: products});
},
handleChange: function(index){
return function(e){
var products = this.state.products.slice();
var product = products[index];
products[index] = Object.assign({}, product, {
value: parseInt(e.target.value, 10)
});
this.setState({products: products});
}.bind(this);
},
getInitialState: function () {
return {
products: this.props.initialProducts.map(function (product) {
return {
name: product.name,
value: 0
}
})
}
},
render: function() {
var prods = this.state.products.map(function(item, id){
return (
<li key={id}>
{item.name}
<input type="number" value={item.value} onChange={this.handleChange(id)} />
</li>
)
}.bind(this));
return (
<ul>
{prods}
<button onClick={this.handleReset}>Reset</button>
</ul>
);
}
});
React.render(<Hello initialProducts={products} />, document.getElementById('container'));
You'll have to call setState every time there is a change in any of the input boxes, and setState when the user intends to reset all inputs.
Also, I did some refactoring. I renamed the products property to initialProducts property. This is because assigning a property to a state is an anti-pattern, unless we clarify that the inputed property will be a part of the state that will change over time.
The idea here is just to get all input and use a for to clean them up. Please note that I'm using Jquery to get all inputs.
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function cleanForm(){
var allInputs = $( ":input" );
for (i = 0; i < allInputs.length; i++) {
allInputs[i].value = "";
}
}
</script>
</head>
<body>
<form>
<input type="text" name="1"/>
<input type="text" name="2"/>
<input type="text" name="3"/>
<input type="text" name="4"/>
<input type="button" name="clean" onclick="cleanForm()" value="clean"/>
</form>
</body>

Resources