Reactive property is not propagating to component in Vue 3 app - javascript

I have a Vue 3 app. I am trying to setup a store for state management. In this app, I have the following files:
app.vue
component.vue
main.js
store.js
These files include the following:
store.js
import { reactive } from 'vue';
const myStore = reactive({
selectedItem: null
});
export default myStore;
main.js
import { createApp } from 'vue';
import App from './app.vue';
import myStore from './store';
const myApp = createApp(App);
myApp.config.globalProperties.$store = myStore;
myApp.mount('#app');
component.vue
<template>
<div>
<div v-if="item">You have selected an item</div>
<div v-else>Please select an item</div>
<button class="btn btn-primary" #click="generateItem">Generate Item</button>
</div>
</template>
<script>
export default {
props: {
item: Object
},
watch: {
item: function(newValue, oldValue) {
alert('The item was updated.');
}
},
methods: {
generateItem() {
const item = {
id:0,
name: 'Some random name'
};
this.$emit('itemSelected', item);
}
}
}
</script>
app.vue
<template>
<component :item="selectedItem" #item-selected="onItemSelected" />
</template>
<script>
import Component form './component.vue';
export default {
components: {
'component': Component
},
data() {
return {
...this.$store
}
},
methods: {
onItemSelected(item) {
console.log('onItemSelected: ');
console.log(item);
this.$store.selectedItem = item;
}
}
}
</script>
The idea is that the app manages state via a reactive object. The object is passed into the component via a property. The component can then update the value of the object when a user clicks the "Generate Item" button.
I can see that the selectedValue is successfully passed down as a property. I have confirmed this by manually setting selectedValue to a dummy value to test. I can also see that the onItemSelected event handler works as expected. This means that events are successfully flowing up. However, when the selectedItem is updated in the event handler, the updated value is not getting passed back down to the component.
What am I doing wrong?

$store.selectedItem stops being reactive here, because it's read once in data:
data() {
return {
...this.$store
}
}
In order for it to stay reactive, it should be either converted to a ref:
data() {
return {
selectedItem: toRef(this.$store, 'selectedItem')
}
}
Or be a computed:
computed: {
selectedItem() {
return this.$store.selectedItem
}
}

Related

Vue 3 Composition api pass data boject to child

I want to pass reactive data object to child, but app shows blank page without error message whatsoever. I want to use composition api.
Parent:
<template>
<Landscape :viewData="viewData"/>
</template>
<script>
import { onMounted, onUnmounted, ref, inject } from 'vue';
export default {
name: 'App',
setup() {
const resizeView = ref(false)
const mobileView = ref(false)
const viewData = reactive({resizeView, mobileView})
viewData.resizeView.value = false
viewData.mobileView.value = false
// lets do sth to change viewData
return {
viewData
}
},
components: {
Landscape
}
}
</script>
Child:
<template>resize- {{viewData.resizeView}} mob {{viewData.mobileView}}
</template>
<script>
export default {
name: 'Header',
props: {
viewData: Object,
},
setup() {
return {
}
}
}
</script>
everything works, when in parent, data object is passed directy like this
<Landscape :viewData="{resizeView: false, mobileView: false}"/>
According to Vue docs about reactive objects:
The reactive conversion is "deep"—it affects all nested properties.
So you don't need to wrap every variable as a ref in reactive object (unless you want to unwrap ref variable). Check Vue docs for more info about reactivity API in Vue.
I provided some basic usage of ref and reactive with your Landscape component. Paste this in your App.vue:
<template>
<button #Click="changeResize" type="button">Change ref values</button>
<Landscape :viewData="viewData" />
<br />
<br />
<button #Click="changeReactiveSize" type="button">
Change reactive values
</button>
<Landscape :viewData="otherViewData" />
</template>
<script>
import { onMounted, onUnmounted, ref, inject } from 'vue';
export default {
name: 'App',
setup() {
const resizeView = ref(false);
const mobileView = ref(false);
const viewData = {
resizeView,
mobileView,
};
const changeResize = () => {
viewData.resizeView.value = !viewData.resizeView.value;
viewData.mobileView.value = !viewData.mobileView.value;
};
const otherViewData = reactive({
mobileView: false,
resizeView: false,
});
const changeReactiveSize = () => {
otherViewData.resizeView = !otherViewData.resizeView;
otherViewData.mobileView = !otherViewData.mobileView;
};
return {
viewData,
otherViewData,
changeResize,
changeReactiveSize,
};
},
components: {
Landscape
}
}
You can also check this code example on stackblitz.

Vue3 Creating Component Instances Programmatically on button click

In vue2 it was be easy:
<template>
<button :class="type"><slot /></button>
</template>
<script>
export default {
name: 'Button',
props: [ 'type' ],
}
</script>
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)
extend + create instance. But in vue3 it's has been deleted. Where are another way?
import {defineComponent,createApp} from 'vue'
buttonView = defineComponent({
extends: Button, data() {
return {
type: "1111"
}
}
})
const div = document.createElement('div');
this.$refs.container.appendChild(div);
createApp(buttonView ).mount(div)
This works for me, using options API, in a method create a component
import { defineComponent } from "vue";
createComponent() {
var component = {
data() {
return {
hello:'world',
}
},
template:`<div>{{hello}}</div>`
}
var instance = defineComponent(component);
}
Use it within your template once you've instantiated it
<component :is="instance" v-if="instance"/>
When I migrated from Element UI to Element Plus I had to pass prefix/suffix icons as components and not as classname strings anymore. But I use single custom icon component and I have to first create a vnode and customize it with props and then pass it in as the
icon prop.
So I created a plugin:
import { createVNode, getCurrentInstance } from 'vue'
export const createComponent = (component, props) => {
try {
if (component?.constructor === String) {
const instance = getCurrentInstance()
return createVNode(instance.appContext.components[component], props)
} else {
return createVNode(component, props)
}
} catch (err) {
console.error('Unable to create VNode', component, props, err)
}
}
export default {
install(APP) {
APP.$createComponent = createComponent
APP.config.globalProperties.$createComponent = createComponent
}
}
And then I can use it like this for globally registered components:
<component :is="$createComponent('my-global-component', { myProp1: 'myValue1', myProp2: 'myValue2' })" />
And for locally imported components:
<component :is="$createComponent(MyComponent, { foo: 'bar' }) />
Or
data() {
return {
customComponent: $createComponent(MyComponent, { foo: 'bar' })
}
}
<template>
<component :is="customComponent" />
<MyOtherComponent :customizedComponent="customComponent" />
</template>
Try out to extend the Button component and then append it root element $el to the referenced container:
import Button from 'Button.vue'
const CompB = {
extends: Button
}
this.$refs.container.appendChild(CompB.$el)

Vue control and update state from another component with vuex

For example, I have a file named App.vue and there I have navigation drawer component. And I have a file named Home.vue with the toolbar component. The thing is that I need to toggle navigation drawer state(true or false) from the Home.vue's toolbar component(also, the navigation drawer component is rendered in Home.vue). The code below doesn't return any error and doesn't change the navigation drawer visibility. Also, if set state manually in store.js, navigation drawer follows it. Can anyone please help?
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
drawer: false
},
mutations: {
toggleDrawer: function(state) {
return state.drawer = !state.drawer;
}
},
actions: {
toggleDrawer({ commit }) {
commit('toggleDrawer');
}
},
getters: {
active: (state) => {
return state.drawer;
}
}
})
App.vue
<v-navigation-drawer v-model="drawer"> ... </v-navigation-drawer>
<script>
import store from './store'
export default {
data: function() {
return {
drawer: this.$store.state.drawer
}
}
}
</script>
Home.vue
<v-toolbar-side-icon #click="$store.commit('toggleDrawer')"> ... </v-toolbar-side-icon>
<script>
import store from '../store'
export default {
data: function() {
return {
drawer: this.$store.state.drawer // Seems like I don't need it here
}
}
}
</script>
This is an older post, but in case anyone come looking for the answer, this seems to work.
from the Vuex guide, Form Handling section, Two-way Computed Property
I modified the codesandbox provided by Sovalina (thanks!) link
The other way is to use v-model
<v-navigation-drawer v-model="drawer" app>
with the two way computed property, instead of mapGetters
<script>
export default {
computed: {
drawer: {
get () {
return this.$store.state.drawer
},
set (value) {
this.$store.commit('toggleDrawer', value)
}
}
}
}
</script>
You can use the navigation drawer's property permanent instead of v-model (to avoid mutate your store outside vuex) and use the getter active you defined.
App.vue:
<template>
<v-app >
<v-navigation-drawer :permanent="active">
...
</v-navigation-drawer>
</v-app>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters([
'active'
])
},
}
</script>
Home.vue:
<template>
<v-toolbar-side-icon #click="toggle"> ... </v-toolbar-side-icon>
</template>
<script>
export default {
methods: {
toggle() {
this.$store.dispatch('toggleDrawer')
}
}
}
</script>
Note: as you defined an action toggleDrawer in your store you can use dispatch instead of commit.
Live example here

Vue: Set child-component data with props values not working

Simply, I have two components:
Parent component which passes a prop object called "profile"
Child component which receives the profile prop
The profile value is an object like this:
{
name: "Something",
email: "some#thing.com"
}
What happens?
The child component receives perfectly the profile value in the template, but it seems impossible to retrieve and set it to the component data.
What is the goal?
I want to initialise the value "email" with the profile email prop.
What did I expect?
export default {
props: ["profile"],
data() {
return {
email: this.profile.email
}
}
}
UPDATE
I haven't specified that email is a data value used as model.
I have just tried to remove it and simply print the value of email in the template and it doesn't work as well.
<!-- PARENT COMPONENT -->
<template>
<dialog-settings ref="dialogSettings" :profile="profile"></dialog-settings>
</template>
<script>
import Auth from "../services/apis/auth";
import DialogSettings from "../components/dialog-settings";
export default {
name: "app",
components: {
"dialog-settings": DialogSettings
},
beforeCreate() {
Auth.checkToken()
.then(profile => {
this.profile = profile;
})
.catch(err => {
});
},
data() {
return {
title: "App",
drawer: true,
profile: {},
navItems: []
};
}
}
</script>
<!-- CHILD COMPONENT -->
<template>
{{profile}} <!-- All the fields are passed and available (e.g. profile.email)-->
{{email}} <!-- Email is not defined -->
</template>
<script>
import Auth from "../services/apis/auth";
import DialogSettings from "../components/dialog-settings";
export default {
name: "dialog-settings",
props: ["profile"],
data() {
return {
email: this.profile.email
}
}
}
</script>
UPDATE 2
I have tried several things and I think that the problem is the asynchronous call to the API in the beforeCreate().
your child component email property should be a computed value
<!-- CHILD COMPONENT -->
<template>
<div>
{{profile}} <!-- All the fields are passed and available (e.g. profile.email)-->
{{email}} <!-- Email is not defined -->
</div>
</template>
<script>
import Auth from "../services/apis/auth";
import DialogSettings from "../components/dialog-settings";
export default {
name: "dialog-settings",
props: ["profile"],
data() {
return {
}
},
computed: {
email () {
return this.profile ? this.profile.email : 'no email yet'
}
}
}
</script>
That's because parent component property is set after rendering child component.
"Data" is not reactive, it's set once when component is created. Prop 'profile" is reactive so first when you render component you should see {} and after response from Auth is set.
If you still want to keep it in data, you could display child component like that:
<dialog-settings ref="dialogSettings" :profile="profile" v-if="profile.email"></dialog-settings>
But i wouldn't recommend that!

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

Categories

Resources