Update a template after state changes using non-vuex state management - javascript

I am making an app which increments a value when you click a + button.
I am following the example from the documentation on Simple State Management.
I have set up an event handling method which increments a state value. This is triggered when a button is clicked. It updates the state value, but the template doesn't update.
To prove this, I have set up console logs in my increment function that fire and reflect the state value as expected. However, the value in the DOM never changes:
I have tried referring to the counterValue in the template as state.counterValue and store.state.counterValue but I get console errors for this.
What am I doing wrong?
Here is my template:
<template>
<div>
<h1>{{store.state.counterValue}}</h1>
<button v-on:click="increment">+</button>
</div>
</template>
Here is my script:
<script>
const store = {
debug: true,
state: {
counterValue: 0
},
increment() {
console.log('updating counterValue...')
this.state.counterValue = this.state.counterValue + 1
console.log(this.state.counterValue)
}
}
export default {
data() {
return {
counterValue: store.state.counterValue
}
},
methods: {
increment: function() {
store.increment()
}
}
}
</script>

The Problem With {{store.state.counterValue}}
From the docs
The mustache tag will be replaced with the value of the msg property on the corresponding data object.
Your data object (i.e. the component/vue-instance) does not have a property named store. To access const store, you need to proxy it through the component:
data() {
return {
store: store
}
},
The Problem With counterValue: store.state.counterValue
This sets this.counterValue equal to the initial value of store.state.counterValue. But there is no code keeping them in sync. So, when store.state.counterValue changes, counterValue will remain the same.
Solution
Proxy const store through the component as explained above. Example:
const store = {
debug: true,
state: {
counterValue: 0
},
increment() {
console.log('updating counterValue...')
this.state.counterValue = this.state.counterValue + 1
console.log(this.state.counterValue)
}
}
new Vue({
el: '#app',
data() {
return {
store: store
}
},
methods: {
increment: function() {
this.store.increment();
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.4/vue.js"></script>
<div id="app">
<h1>{{store.state.counterValue}}</h1>
<button v-on:click="increment">+</button>
</div>

Related

Vue.js : Passing Props Down to Child Components to Update Styles in the DOM

I'm a beginner trying to get my app to pass props that set CSS styles down a chain to child components. I have a listener that checks for view port size, and as the window gets resized, it checks past a certain point and then swaps the css class and passes it down the chain..
I think I may be doing something incorrectly because my child components don't seem to be receiving the new styles and aren't updating in the DOM as I drag the window.
Here is my code.. I removed irrelevant code to make it easier to read:
Page_Listings.vue
<template>
<main>
<section>
<ListingRack
:prp_classes="rackClass"
/>
</section>
</main>
</template>
<script>
import ListingRack from './Listing__Rack.vue';
export default {
name: 'Front_Page__Panel',
data() {
return {
viewportWidth: window.innerWidth
}
},
methods: {},
mounted() { window.onresize = () => this.viewportWidth = window.innerWidth },
components: {ListingRack},
},
computed: {
rackClass: function(){
let theValue;
console.log('>> viewport width is now: ',this.viewportWidth)
if(this.viewportWidth > 1200) {
theValue = "grid_view";
console.log('>> grid view')
}
else {
theValue = 'card_view';
console.log('>> card view')
}
return theValue
}
}
}
</script>
Listing__Rack.vue
<template>
<div class="listing_rack" :class="classes">
<ul>
<li v-for="item in listings" :key="item.postId">
// I removed irrelevant code for hte sake of simplicity in this example.
// listings is a GraphQL returned array of data that generates a list of "listings".
<Listing
:prp_classes=classes
/>
</li>
</ul>
</div>
</template>
<script>
import Listing from './Listing.vue'
export default {
name: 'listing__rack',
data() {
return {
posts: [], // what we get from the database.
listings: [], // what we copy from the database.
classes: this.prp_classes
}
},
props: {
prp_classes: String
},
components: {
Listing
},
watch: {
classes: function(){
//just to check if we're receiving anything...
console.log(">> [Listing_Rack](watch)(classes) there was a change to classes!");
}
}
}
</script>
Listing.vue
<template>
<div :id=id
:class=classes
class="listing"
:style="backgroundStyle"
>
</div>
</template>
<script>
export default {
name: 'Listing',
data() {
return {
classes: this.prp_classes,
backgroundStyle: String
}
},
props: {
prp_classes: String
},
methods: {
checkClasses: function(){
if(this.classes === 'grid_view') this.backgroundStyle = 'background: center / cover no-repeat url(background.jpg);';
}
},
mounted: function() {
this.checkClasses();
},
watch: {
classes: function(){
this.checkClasses();
}
}
}
</script>
My console.logs on rackClass so I know the class swapping part is working, but all my subsequent child components don't seem to be updating accordingly..
Can someone tell me what I'm doing wrong? Is there a better way to do this? How come my props aren't being passed when I drag the window, and how can I dynamically set styles in the DOM?
Your code does not work because of the one big mistake (don't worry, many people do it)
You are passing your classes using props to child components. But instead of using this prop (prp_classes) directly in the child's template, you create an absolutely unnecessary classes property in the data()
Problem with that is that data() is executed only once when the component is created. If the value of the prp_classes prop changes later, classes property from the data() just holds the old value.
To fix this, remove unnecessary classes from the data and use the prop directly in the template...
...bit more explanation by example what is going on:
let prp_classes = 'card_view'
let classes = prp_classes
prp_classes = 'grid_view'
// prp_classes === 'grid_view', classes === 'card_view', prp_classes !== classes
// strings/numbers/Date ...all work the same
let o1 = { a: 1 }
let o2 = o1
o1.a = 2
// o1.a === 2, o2.a === 2, o1 === o2
More to study

Vue Setting A Method Name Based on a Prop Value?

Is it possible to set the name of a Method in my Vue application based on the value of a prop? So my Vue Application looks like:
<script>
export default {
name: 'CheckboxFilter',
props: {
tax: '',
identifier: 'Category'
}
}
</script>
Then in my Methods I want to name a method based on the value of the identifier prop.
methods: {
updateSelected + this.identifier (e) {
this.$store.commit('updateSelectedCategories', e.target.value)
}
}
Is something like this possible to do where the name of the method is influenced by the value of a prop?
EDIT: the reason why I am trying to do this is I have a component that has checkbox input filters. The layout of this component will always be the same the only difference will be the values present on those checkboxes. I need to keep track of which checkboxes are select independently from each component. So my thought for doing this is to pass an identifier prop to use as a naming convention for the method where I store the value of the selected checkboxes for each component independently in my Vuex store.
Instead, pass it as an argument to your vuex store:
const store = new Vuex.Store({
state: {
attribute: {
tag: true,
bag: false
}
},
mutations: {
setAttr(state, { value, attribute }) {
state.attribute[attribute] = value;
}
}
});
Vue.component('custom-checkbox', {
props: ['attribute'],
template: '<div><input type="checkbox" v-model="checkBox">{{attribute}}</div>',
computed: {
checkBox: {
get() {
return this.$store.state.attribute[this.attribute]
},
set(value) {
this.$store.commit('updateMessage', {
value,
attribute: this.attribute
});
}
}
}
});
new Vue({
el: '#app',
store,
template: '<div><custom-checkbox attribute="tag" /><custom-checkbox attribute="bag" /></div>'
});
<html>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.0/vuex.min.js"></script>
</body>
</html>
That way, you're not trying to rely on dynamic data in your static code.

Dynamically changing props

On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/

Linking radio buttons to data in Vue with render/createElement

When creating radio buttons in Vue with the render/createElement function, how can you link the value of the selected radio button to the intended variable in the data object in the model?
You will have to do it manually. See Vue's documentation on v-model on the render function page.
There is no direct v-model counterpart in render functions - you
will have to implement the logic yourself
Here is a really lose example that will emit the value of the clicked radio input. You'll still need to solve ensuring that the emitted value is the actually selected radio as well as setting the selected value if that is designated as a prop.
const foo = {
props: ["name"],
render(h) {
const $this = this;
const arr = ["a", "b", "c"];
return h("div", {}, arr.map(v => h("label", [h("input", {
domProps: {
type: "radio",
name: this.name
},
on: {
click() {
$this.$emit("change", v);
}
}
}), h("span", v)])));
}
};
const app = new Vue({
el: "#app",
data() {
return {
fizz: true
};
},
components: {
foo
},
methods: {
onChange(e) {
console.log(e);
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<foo name="some" #change="onChange"></foo>
</div>

vuejs passing array of checkboxes to parent template is only passing one value

I looked at potential dupes of this, such as this one and it doesn't necessarily solve my issue.
My scenario is that I have a component of orgs with label and checkbox attached to a v-model. That component will be used in combination of other form components. Currently, it works - but it only passes back one value to the parent even when both checkboxes are click.
Form page:
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs,
},
methods: {
submit: function () {
console.log(this.$data)
}
}
}
</script>
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" name="selectedOrgs[]" v-on:input="$emit('input', $event.target.value)" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Currently there are two fake orgs in the database with an ID of 1 and 2. Upon clicking both checkboxes and clicking the submit button, the selectedOrgs array only contains 2 as though the second click actually over-wrote the first. I have verified this by only checking one box and hitting submit and the value of 1 or 2 is passed. It seems that the array method works at the component level but not on the component to parent level.
Any help is appreciated.
UPDATE
Thanks to the comment from puelo I switched my orgListing component to emit the array that is attached to the v-model like so:
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: [],
selectedOrgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
Then on the other end I am merely console.log() the return. This "works" but has one downside, it seems that the $emit is being fired before the value of selectedOrgs has been updated so it's always one "check" behind. Effectively,I want the emit to wait until the $data object has been updated, is this possible?
Thank you so much to #puelo for the help, it helped clarify some things but didn't necessarily solve my problem. As what I wanted was the simplicity of v-model on the checkboxes populating an array and then to pass that up to the parent all while keeping encapsulation.
So, I made a small change:
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" v-model="selectedOrgs" name="selectedOrgs[]" v-on:change="updateOrgs" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Form Page
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs" v-on:updateOrgs="updateSelectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs
},
methods: {
updateSelectedOrgs: function (org) {
console.log(org)
},
submit: function () {
console.log(this.$data)
}
}
}
</script>
What the primary change here is I now fire a method of updateOrgs when the checkbox is clicked and I pass the entire selectedOrgs array via the this.$emit('updateOrgs', this.$data.selectedOrgs)`
This takes advantage of v-model maintaining the array of whether they're checked or not. Then on the forms page I simply listen for this event on the component using v-on:updateOrgs="updateSelectedOrgs" which contains the populated array and maintains encapsulation.
The documentation for v-model in form binding still applies to custom components, as in:
v-model is essentially syntax sugar for updating data on user input
events...
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage and
https://v2.vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model
So in your code
<list-orgs v-model="selectedOrgs"></list-orgs>
gets translated to:
<list-orgs :value="selectedOrgs" #input="selectedOrgs = $event.target.value"></list-orgs>
This means that each emit inside v-on:input="$emit('input', $event.target.value) is actually overwriting the array with only a single value: the state of the checkbox.
EDIT to address the comment:
Maybe don't use v-model at all and only listen to one event like #orgSelectionChange="onOrgSelectionChanged".
Then you can emit an object with the state of the checkbox and the id of the org (to prevent duplicates):
v-on:input="$emit('orgSelectionChanged', {id: org.id, state: $event.target.value})"
And finally the method on the other end check for duplicates:
onOrgSelectionChanged: function (orgState) {
const index = selectedOrgs.findIndex((org) => { return org.id === orgState.id })
if (index >= 0) selectedOrgs.splice(index, 1, orgState)
else selectedOrgs.push(orgState)
}
This is very basic and not tested, but should give you an idea of how to maybe solve this.

Categories

Resources