I have a addToCart component(child) on foodList component(parent). and there is another component Cart. i want to reset the addToCart component's counter value to 0 whenever i will empty my cart.
App.vue
data() {
return {
msg: "Welcome to Your Food Ordering App",
foodData:[],
cart:[],
reset:false
};
},
methods: {
emptyCart:function(){
this.reset = true;
this.cart = [];
}
}
foodList.vue
export default {
props:['foods','reset'],
data() {
return {
};
}
}
<addToCart :reset="reset"></addToCart>
addToCart
export default {
props:['food','reset'],
data(){
return {
counter:0
}
},
beforeMount() {
if(this.reset) {
this.counter = 0;
}
}
in app.vue I'm modifying the reset property to "true" and then passing it to foodList.vue, then passing it to addToCart.vue.
In addToCart.vue I'm checking if reset prop is true then set the counter to 0;
But this is not working.let me know where am I missing?
Please refer to this link for complete code.
food ordering app
So basically you want to pass the state over multiple components. There are multiple ways to achieve this. These are my three recommend ones.
Centralized State management
In order to handle states easier, you can make use of a centralized state management tool like vuex: https://github.com/vuejs/vuex
This is what I recommend you, especially when it comes to bigger applications, where you need to pass the state over multiple levels of components. Trust me, this makes your life a lot easier.
Property binding
The most basic way to communicate with your child components is property binding. But especially when it comes to multi-level communication it can get quite messy.
In this case, you would simply add counter to both of your child components props array like this:
foodList.vue (1. Level Child Component)
export default {
props:['foods','reset', 'counter'],
// ... your stuff
}
And include the component like this:
<foodList :counter="counter"></foodList>
addToCart.vue (2. Level Child Component)
export default {
props:['food','reset', 'counter'],
// ... your stuff
}
And finally include the component like this:
<addToCart :reset="reset" :counter="counter"></addToCart>
As a last step, you can specify counter in the data object of your root component and then modify it on a certain event. The state will be passed down.
App.vue
data() {
return {
// ... your stuff
counter: 0,
};
},
methods: {
emptyCart:function(){
// ... your stuff
this.counter = 0; // reset the counter from your parent component
}
}
Event Bus
As a third option, you could make use of Vue's event bus. This is the option I personally choose for applications, which get too messy with simple property binding, but still are kind of too small to make us of Centralized State management.
To get started create a file called event-bus.js and then add the following code to it:
import Vue from 'vue';
export const EventBus = new Vue();
Now you can simply trigger events from your parent Component like this:
App.vue
import { EventBus } from './event-bus.js'; // check the path
export default {
// ... your stuff
methods: {
emptyCart:function(){
// ... your stuff
EventBus.$emit('counter-changed', 0); // trigger counter-changed event
}
}
}
And then listen to the counter-changed event in your child component.
addToCart.vue
import { EventBus } from './event-bus.js';
export default {
// ... your stuff
created() {
EventBus.$on('counter-changed', newCounter => {
this.counter = newCounter;
});
}
}
Learn more about the event bus: https://alligator.io/vuejs/global-event-bus/
Related
I am working on my first Vue project. I'm used to React and vanilla js, but just getting my head around a few concepts in Vue here.
In particular, importing state and action props from a Pinia store, and seemingly having to import those multiple times in a single Vue component (something I don't need to do in React).
In this example, I am importing a simple count value, and an increment function, and trying to use these in a few different places:
<script setup>
// I import everything initially in setup, which works fine,
// and these props (currentCount and incrementCount)
// can be used in my template:
import { storeToRefs } from 'pinia';
import { useStore } from '#/stores/store';
const { currentCount } = storeToRefs(useStore());
const { incrementCount } = useStore();
</script>
<template>
<main>
Current count: {{ currentCount }}
<button #click="incrementCount">Increment</button>
</main>
</template>
<script>
// I can't use store values from setup here.
// This doesn't work:
// console.log(currentCount);
// I also can't import store values here.
// I get the following error:
// "getActivePinia was called with no active Pinia"
// const { currentCount } = storeToRefs(useStore());
export default {
mounted() {
// I have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
watch: {
// weirdly, this reference to watching "currentCount" works:
currentCount() {
// I also have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
},
};
</script>
As you can see, if I want to use store values in my template, on mount, and in a watcher (whereby I'd use React's useEffect hook) I am having to import the store props 3 times in total.
Is this correct / normal? Is there a simpler way to achieve what I'm doing, where I only import props once? I want to be sure I haven't missed something and am not doing something in an unusual way.
Thanks for any help and advice!
Pinia was designed with Composition API in mind.
So its intended usage is inside setup() function, where you'd only import it once.
To use it outside of a setup() function, you have two main routes:
inside components, you can just return it from setup() and it becomes available in any hook/method/getter. Either as this.store or spread:
import { useStore } from '#/store'
import { toRefs } from 'vue'
// or from '#vue/composition-api' in Vue2
export default {
setup: () => ({ ...toRefs(useStore()) })
}
/* this makes every state prop, getter or action directly available
on current component instance. In your case, `this.currentCount`.
Obviously, you can also make the entire store available as `this.someStore`:
setup: () => ({ someStore: useSomeStore() })
// now you can use `this.someStore` anywhere
*/
a more general approach is to export the pinia instance (returned by createPinia()), from main.(js|ts), import it where you need the store and then call useStore() passing the pinia instance as an argument.
This can be done anywhere, even outside of components.
Generic example:
import { pinia } from 'main.js'
import { useSomeStore } from '#/store'
const someStore = useSomeStore(pinia);
I should probably also mention the mapState helper provided by pinia. It allows you to select only a few of the keys exposed to current instance. Example:
import { mapState } from 'pinia'
// ...
computed: {
...mapState(useSomeStore, [ 'currentCount'])
}
// Now `this.currentCount` is available
Note: mapState is weirdly named, as it allows you to access more than just state props (also getters and actions). It was named mapState to match the similar helper from vuex.
An even more general approach is to add your store as global, using the plugin registration API in Vue2:
import { useSomeStore } from '#/store';
import { createPinia } from 'pinia';
const pinia = createPinia();
const someStorePlugin = {
install(Vue, options) {
Vue.prototype.someStore = useSomeStore(options.pinia)
}
};
Vue.use(someStorePlugin, { pinia });
new Vue({ pinia });
After this, every single component of your Vue instance will have this.someStore available on it, without you needing to import it.
Note: I haven't tested adding a store in globals (and I definitely advise against it - you should avoid globals), but i expect it to work.
If you want to combine pinia stores with the options API, one way to do it is to use the setup() function inside the options to call useStore:
<script>
import { useStore } from '#/stores/store';
export default {
setup() {
const store = useStore();
return {store}
},
watch: {
store.currentBrightness(newVal, oldVal){
// your code
}
},
methods: {
// inside methods use this.store
},
mounted() {
console.log(this.store.currentCount);
}
}
</script>
Some might consider this as a unwanted mix of composition and options API, but in my view it is a quite good solution for pinia stores.
Nechoj, has the most straightforward answer. Also if you have multiple stores you can always import the stores as necessary into a parent component then use inject just add some parts. For example I have a route data that is called via an api, I don't need it everywhere all the time so i call it in a parent then use inject to use those routes in a drop down that might be a great grandchild component. I don't need that whole utils store just the routes.
index page:
import { useUtilsStore } from "src/stores/utilsStore";
const passengerRoutes = computed(() => utilsStore.getPassengerRoutes);
provide("passengerRoutes", passengerRoutes);
grandchild component:
const compRoutes = inject("passengerRoutes");
In my store module /store/template.js I have:
const templateConfig = {
branding: {
button: {
secondary: {
background_color: '#603314',
background_image: ''
}
}
}
}
export const state = () => ({
branding: {},
...
})
export const actions = {
initializeStore (state) {
state.branding = templateConfig.branding
}
}
(initializeStore() is called when app initially loads)
I want to retrieve the branding the branding object in my component:
computed: {
...mapState({
branding: state => state.template.branding
})
}
But when trying to console.log() branding I see this:
Why don't I simply see the branding object? (and what on earth is this?)
You need to always use a mutation to change state. You can call one from your action:
export const mutations = {
SET_BRANDING(state, payload) {
state.branding = payload;
}
}
export const actions = {
initializeStore ({ commit }) {
commit('SET_BRANDING', templateConfig.branding);
}
}
What you're seeing with the observer is normal, and indicates that the branding object has been successfully mapped and accessed.
What you see is Vue's observable object, which is how Vue implements reactivity. Without this, there would be no reactivity, and you will see such a wrapper on all top-level reactive objects. You can pretend it's not there.
Vue in fact applies this same "wrapper" to the data object internally to make it observable:
Internally, Vue uses this on the object returned by the data function.
You won't see it on other reactive properties, but if they're reactive, they belong to some parent observable object.
You need to import { mapState, mapActions } from 'vuex' (already done I guess).
And then, you can write this
...mapState(['branding']) // or ...mapState('#namespacedModule', ['branding'])
Still, why do you not simply put the state directly (with your background_color) rather than going through a Vuex action ?
If you want to keep it this way, do not forget to await this.initializeStore() in your component before trying to access the state.
I'm trying to learn vuex but I think I am missing some basic understanding. Any advice please.
From one component I am dispatching the scale value of my zoomable map to vuex store.
Store.js
export default new Vuex.Store({
state: {
scale:""
},
getters:{
MJERILO: state => {
return state.scale
}
},
mutations:{
STORESCALE: (state, payload) => {
state.scale = payload
}
},
actions:{
RECEIVECURRENTSCALE: (context, payload) => {
context.commit("STORESCALE", payload);
}
}
})
This part is working well because in vue dev tool I can see that the scale number is changing in mutation, state and getters when I do zoom in/out with my mouse. (Do, in mutation is changing automaticaly, and for state and getters I need to press load state. I guess this work like this)
So the problem is probably in the way how I am trying to receive data from vuex state into some other component.
I tried:
Map1.vue
mounted(){
var scale = this.$store.getters.MJERILO
}
But I just get the value stored in state property mjerilo (in this case empty). And I need dynamic that I sent to state.
For static data this worked perfectly (I tried with simple array).
I also tried to retry data in computed, but I have a similar problem. In this case in mounted I get just the first scale value
computed: {
mjerilo(){
return this.$store.getters.MJERILO
}
}
mounted(){
var scale = this.mjerilo
}
I am quite lost. From readings I understand that when ever I scroll my map with mouse I am sending data to action for "registration", than through mutation I am storing this data in state. From state I can get the last updated data (in this case scale) in any other vue component of my app?
UPDATE: I am adding Map1.vue component
<template>
<svg-map name="Zupanije" ></svg-map>
</template>
<script>
import * as d3 from 'd3'
import SvgMap from "./SvgMap"
export default {
name: "Map1",
components: {
"svg-map":SvgMap
},
mounted(){
......lots of javascrip code
.
.
var scale = this.$store.getters.MJERILO
}
}
</script>
I believe you are looking for a function like watch
data: {
scale: this.$state.scale
},
watch: {
'$store.state.scale': (newVal) => {
this.scale = newVal;
}
}
I'm not sure what's wrong but you could try this, what happens then? How you dispatch your action?
<template>
<div>
{{ MJERILO }}
</div>
</template>
import { mapGetters, mapActions } from "vuex";
export default {
...mapGetters(["MJERILO"]),
}
What I'm trying to accomplish
I'm trying to figure out a good architecture for the application I'm working on.
If you look at some screenshots of the current version of our application you'll see tables/lists of data in various formats. The table in the second screenshot has its own navigation. In some cases, table rows can be selected. In other screens, the list of items can be filtered on specified keywords, and so on.
The front-end is going to be rebuilt in VueJS. Instead of building one-off components for each specific screen, I'd like to have a more generic solution that I can use throughout the application.
What I need is a system of components where the root contains a set of data and the children provide (possibly multiple) representations of that data. The root component also contains – mostly user-specified – config, filters and selections which is used to control the representations.
What I'm trying to achieve is best illustrated with some pseudocode.
Questions
What is the best way to store the data? How should the child components access this data?
Passing the data-display's data down with props to each of its children seems cumbersome to me. Especially since the representations have implicit access to this data.
Should I use Vuex for this? How would that work with multiple instances of data-display? I read something about dynamic module registration, could that be useful?
Are there better ways? Any help is greatly appreciated!
I'm not sure I grasp all the details of your requirements, but understand data is common across multiple 'pages' in the SPA.
I would definitely recommend Vuex:
same data for all pages
access via component computed properties,
which are reactive to store changes but also cached to save
recomputing if no changes in dependencies
saves a lot of potentially complex passing of data in props (passing downwards) and events (passing upwards)
Looking at the pseudocode, you have three types of filters on the sidebar. Put that data in the store as well, then computed properties on views can apply filters to the data. Computation logic is then easily tested in isolation.
child.vue
<template>
<div>{{ myChildData.someProperty }}</div>
</template>
<script>
export default {
props: [...],
data: function () {
return {
selection: 'someValue' // set this by, e.g, click on template
}
},
computed: {
myChildData() {
return this.$store.getters.filteredData(this.selection)
}
}
}
</script>
store.js
const state = {
myData: {
...
},
filters: { // set by dispatch from sidebar component
keyword: ...,
toggle: ...,
range: ...
},
}
const getters = {
filteredData: state => {
return childSelection => { // this extra layer allows param to be passed
return state.myData
.filter(item => item.someProp === state.filters.keyword)
.filter(item => item.anotherProp === childSelection)
}
},
}
const mutations = {
'SET_DATA' (state, payload) {
...
},
'SET_FILTER' (state, payload) {
...
}
}
export default {
state,
getters,
mutations,
}
Generic children
There are probably many ways to tackle this. I've used composition, which means a thin component for each child each using a common component, but you only need this if the child has specific data or some unique markup which can be put into a slot.
child1.vue
<template>
<page-common :childtype="childType">
<button class="this-childs-button" slot="buttons"></button>
</page-common>
</template>
<script>
import PageCommon from './page-common.vue'
export default {
props: [...],
data: function () {
return {
childType: 'someType'
}
},
components: {
'page-common': PageCommon,
}
}
</script>
page-common.vue
<template>
...
<slot name="buttons"></slot>
</template>
<script>
export default {
props: [
'childtype'
],
data: function () {
return {
...
}
},
}
</script>
Multiple Instances of Data
Again, many variations - I used an object with properties as index, so store becomes
store.js
const state = {
myData: {
page1: ..., // be sure to explicitly name the pages here
page2: ..., // otherwise Vuex cannot make them reactive
page3: ...,
},
filters: { // set by dispatch from sidebar component
keyword: ...,
toggle: ...,
range: ...
},
}
const getters = {
filteredData: state => {
return page, childSelection => { // this extra layer allows param to be passed
return state.myData[page] // sub-select the data required
.filter(item => item.someProp === state.filters.keyword)
.filter(item => item.anotherProp === childSelection)
}
},
}
const mutations = {
'SET_DATA' (state, payload) {
state.myData[payload.page] = payload.myData
},
'SET_FILTER' (state, payload) {
...
}
}
export default {
state,
getters,
mutations,
}
Let's say I've got an app with two reducers - tables and footer combined using combineReducers().
When I click on some button two actions are being dispatched - one after another: "REFRESH_TABLES" and "REFRESH_FOOTER".
tables reducer is listening for the first action and it modifies the state of tables. The second action triggers footer reducer. The thing is it needs current state of tables in order to do it's thing.
My implementation looks something like below.
Button component:
import React from 'react';
const refreshButton = React.createClass({
refresh () {
this.props.refreshTables();
this.props.refreshFooter(this.props.tables);
},
render() {
return (
<button onClick={this.refresh}>Refresh</button>
)
}
});
export default refreshButton;
ActionCreators:
export function refreshTables() {
return {
type: REFRESH_TABLES
}
}
export function refreshFooter(tables) {
return {
type: REFRESH_FOOTER,
tables
}
}
The problem is that the props didn't update at this point so the state of tables that footer reducer gets is also not updated yet and it contains the data form before the tables reducer run.
So how do I get a fresh state to the reducer when multiple actions are dispatched one after another from the view?
Seems you need to handle the actions async so you can use a custom middleware like redux-thuk to do something like this:
actions.js
function refreshTables() {
return {
type: REFRESH_TABLES
}
}
function refreshFooter(tables) {
return {
type: REFRESH_FOOTER,
tables
}
}
export function refresh() {
return function (dispatch, getState) {
dispatch(refreshTables())
.then(() => dispatch(refreshFooter(getState().tables)))
}
}
component
const refreshButton = React.createClass({
refresh () {
this.props.refresh();
},
{/* ... */}
});
Although splitting it asynchronous may help, the issue may be in the fact that you are using combineReducers. You should not have to rely on the tables from props, you want to use the source of truth which is state.
You need to look at rewriting the root reducer so you have access to all of state. I have done so by writing it like this.
const rootReducer = (state, action) => ({
tables: tableReducer(state.tables, action, state),
footer: footerReducer(state.footer, action, state)
});
With that you now have access to full state in both reducers so you shouldn't have to pass it around from props.
Your reducer could then looks like this.
const footerReducer = (state, action, { tables }) => {
...
};
That way you are not actually pulling in all parts of state as it starts to grow and only access what you need.