Vue.js passing store data as prop causes mutation warnings - javascript

I'm passing store data (Vuex) as a property of component but it's giving me mutation errors even though I'm not changing the data.
Edit: Codepen illustrating error: https://codesandbox.io/s/v8onvz427l
Input
<template>
<div>
<input type="text" class="form-control" ref="input" />
<div style="padding-top: 5px">
<button #click="create" class="btn btn-primary btn-small">Create</button>
</div>
{{ example }}
</div>
</template>
<script>
import store from "#/store"
export default {
props: {
"example": {
}
},
data() {
return {
store
}
},
methods: {
create() {
store.commit("general_set_creation_name", {name: this.$refs.input.value})
}
}
}
</script>
Modal.vue
<template src="./Modal.html"></template>
<script>
import $ from 'jquery'
import store from '#/store'
export default {
props: {
"id": String,
"height": {
type: String,
default: "auto"
},
"width": {
type: String,
default: "40vw"
},
"position": {
type: String,
default: "absolute"
},
"component": {
default: null
},
"global": {
default: true
}
},
data () {
return {
store: store
}
},
computed: {
body () {
return store.state.General.modal.body
},
props () {
return store.state.General.modal.props
},
title () {
return store.state.General.modal.title
},
},
methods: {
close_modal (event) {
if (event.target === event.currentTarget) {
this.$refs.main.style.display = "none"
}
}
}
}
</script>
<style scoped lang="scss" src="./Modal.scss"></style>
Modal.html
<div
:id="id"
class="main"
ref="main"
#click="close_modal"
>
<div ref="content" class="content" :style="{minHeight: height, minWidth: width, position}">
<div ref="title" class="title" v-if="title">
{{ title }}
</div>
<hr v-if="title" />
<div ref="body" class="body">
<slot></slot>
<component v-if="global" :is="body" v-bind="props"></component>
</div>
</div>
</div>
Changing store data with this line in a third component:
store.commit("general_set_modal", {body: Input, title: "New "+page, props: {example: "example text 2"})

I'm quite sure you should not put a vue component on the state. If you are supposed to do that then I don't think the creators of vuex understand how an event store works.
In the documentation it also says you need to initialize your state with values and you don't do that.
Your sandbox works fine when removing the vue component from the state (state should contain data but vue components are objects with both data and behavior).
index.js in store:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
modal: {
body: {},
title: "",//det it to something
props: {}
},
creationName: null
},
mutations: {
general_set_creation_name(state, payload) {
state.creationName = payload.name;
},
general_set_modal(state, payload) {
state.modal.title = payload.title;
state.modal.props = payload.props;
console.log("we are fine here");
}
},
strict: process.env.NODE_ENV !== "production"
});

For whatever reason, changing the way I import the class removes the warning
const test = () => import('./Test')
Details:
https://forum.vuejs.org/t/getting-vuex-mutation-error-when-im-only-reading-the-data/27655/11

Related

How to pass data after page loading to components in VueJS?

In my project I use Vue.js and Nuxt.js and I have this page.
This page is settings page, where user can changes his settings. As you can see, this is only one page, where user can switch between tabs.
<template>
<div class="account-wrapper">
<div class="avatar" #click="redirect('/account/me')">
<img class='avatar-box' src="../../../assets/img/testava.jpg" alt="ava">
<div class="avatar-text">
<h2 class="nmp">{{ personalSettings.username }}</h2>
<p class="paragraph opacity nmp">Public profile</p>
</div>
</div>
<div class="side-bar">
<div v-for="item in accountHeaderItems" :key="item.title" class="flex">
<div v-if="item.active" class="vertical-line" />
<p :class="[item.active ? 'item item-active' : 'item']" #click="changeSubsection(item)">
{{ item.title }}
</p>
</div>
</div>
<personal-information v-if="currentSection === 'Public account'" :personal-settings="personalSettings" />
<security-settings v-else-if="currentSection === 'Security settings'" :security-settings="securitySettings" />
<site-settings v-else />
</div>
</template>
<script>
import SecuritySettings from '~/components/pageComponents/settings/SecuritySettings'
import PersonalInformation from '~/components/pageComponents/settings/PersonalInformation'
import SiteSettings from '~/components/pageComponents/settings/SiteSettings'
import { getUserSettings } from "~/api";
export default {
name: 'Settings',
components: {
SecuritySettings,
PersonalInformation,
SiteSettings
},
data() {
return {
accountHeaderItems: [
{ title: 'Public account', active: true },
{ title: 'Security settings', active: false },
{ title: 'Appearance settings', active: false },
{ title: 'Notifications', active: false }
],
currentSection: 'Public account',
personalSettings: {},
securitySettings: {},
}
},
async mounted() {
if (localStorage.getItem('token') !== null) await this.getUsersSettings(localStorage.getItem('token'))
else await this.$router.push('/')
},
methods: {
async getUsersSettings(token) {
const userSettings = await getUserSettings(token)
if (userSettings.status === -1)
return this.$router.push('/')
this.personalSettings = userSettings.personalSettings
this.securitySettings = userSettings.securitySettings
},
changeSubsection(item) {
this.currentSection = item.title
this.accountHeaderItems.forEach(header => {
header.active = item.title === header.title
})
},
redirect(path) {
this.$router.push(path)
},
}
}
</script>
The problem is when page loads. When in async mounted() I get data I want to pass it to my components. And here is the problem, when I try to do that it seems to work fine, but there is strange behaviour, I always need to switch between tabs, to make data be visible on page.
For example - in personalSettings object there is field first_name. So, in personal-information component in custom Input I want to show this data in this way (in mounted I make copy of object to prevent mutations):
<Input
v-model="personalInfo.first_name"
:title="'First name'"
:title-class="'small'"
:additional-class="'small'"
/>
...
props: {
personalSettings: {
type: Object,
default: () => {}
}
},
data() {
return {
personalInfo: {},
loading: false,
showPopup: false
}
},
mounted() {
this.personalInfo = this.personalSettings
},
Everything seems to be fine, but, actually, I have to switch to another tab and switch back to this tab to see this data. What's wrong? How can I prevent this behaviour and show data in correct way?
There are many ways to do it, you can use Store, and emit changes and Data you want to use late.
See: https://vuex.vuejs.org/guide/#the-simplest-store

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

How to write a plugin that shows a modal popup using vue. Call should be made as a function()

I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.
Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>
AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>
This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue

How to pass initial form values to child component in Vue.js?

I'm using Vue.js. From my template I include the child component (componentB) which includes several input elements. I want to initialize those input elements from my parent template. I found a way to do this (see code below). However, I'm wondering if this is a correct way, as the articles I have read so far use different approaches (e.g. with $emit):
https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
https://zaengle.com/blog/using-v-model-on-nested-vue-components
https://alligator.io/vuejs/add-v-model-support/
Can you confirm that my code below matches the Vue.js design concepts or are there flaws?
<template>
<div>
<div class="md-layout">
<div class="md-layout-item md-size-100">
<ComponentB ref="componentB" v-model="componentB"></ComponentB>
</div>
</div>
</div>
</template>
<script>
import { ComponentB } from "#/components";
export default {
components: {
ComponentB
},
data() {
return {
componentB: {
textInputField: "my-initial-value"
}
};
},
methods: {
validate() {
return this.$refs.componentB.validate().then(res => {
this.$emit("on-validated", res);
return res;
});
}
}
};
</script>
<style></style>
Form componentB
<template>
<div>
<md-field
:class="[
{ 'md-valid': !errors.has('textInputField') && touched.textInputField },
{ 'md-form-group': true },
{ 'md-error': errors.has('textInputField') }
]"
>
<md-icon>label_important</md-icon>
<label>My text input</label>
<md-input
v-model="textInputField"
data-vv-name="textInputField"
type="text"
name="textInputField"
required
v-validate="modelValidations.textInputField"
>
</md-input>
<slide-y-down-transition>
<md-icon class="error" v-show="errors.has('textInputField')"
>close</md-icon
>
</slide-y-down-transition>
<slide-y-down-transition>
<md-icon
class="success"
v-show="!errors.has('textInputField') && touched.textInputField"
>done</md-icon
>
</slide-y-down-transition>
</md-field>
</div>
</template>
<script>
import { SlideYDownTransition } from "vue2-transitions";
export default {
name: "componentB",
props: ['value'],
components: {
SlideYDownTransition
},
computed: {
textInputField: {
get() {return this.value.textInputField},
set(textInputField) { this.$emit('input', { ...this.value, ['textInputField']: textInputField })}
}
},
data() {
return {
touched: {
textInputField: false
},
modelValidations: {
textInputField: {
required: true,
min: 5
}
}
};
},
methods: {
getError(fieldName) {
return this.errors.first(fieldName);
},
validate() {
return this.$validator.validateAll().then(res => {
return res;
});
}
},
watch: {
textInputField() {
this.touched.runnerName = true;
}
}
};
</script>
<style></style>
The simplest way to pass data to child component is to use props, which are then available in the child component and can pass the values back up to the parent.
https://v2.vuejs.org/v2/guide/components-props.html
// PARENT COMPONENT
<ComponentB :textInputField="textInputField" ...></ComponentB>
// CHILD COMPONENT
// TEMPLATE SECTION
<md-input
v-model="textInputField"
value="textInputField"
...
>
// SCRIPT SECTION
export default {
props: {
textInputField: String
}
}

2 way data binding with Vuex

In the code below I'm 2 way binding the output of a textarea into a p element, once from the component's internal state and once from Vuex. The Vuex state does show the initial value, but the value doesn't update as I add or delete text (as it does correctly with the 1st textarea bound to the internal data). What is the difference that is causing this issue?
Component code:
<template>
<div>
<div>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="internal_state"></textarea>
<p> {{ internal_state }}</p>
<hr>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="this.$store.state.vuex_state"></textarea>
<p> {{ this.$store.state.vuex_state }}</p>
<hr>
</div>
</div>
</template>
<script>
export default {
name: 'WriteArea',
data () {
return {
internal_state: ''
}
},
methods: {
}
}
</script>
Vuex code:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
strict: true,
state: {
counter: 0,
vuex_state: 'starting string'
},
getters: {
vuex_getter1: (state) => {
return state.vuex_string
}
}
})
Vuex state should be updated via a mutation. See the documentation for this exact problem. Solution is not to use v-model, but instead to bind to the :value of the textarea and then have a custom event to mutate the Vuex state on input:
https://vuex.vuejs.org/en/forms.html
<input :value="message" #input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
The other option is to create a setter and getter in the same computed property:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
You can try to use mine library for 2 way binding vuex problem solution
https://github.com/yarsky-tgz/vuex-dot
Example:
<template>
<input v-model="name"/>
<input v-model="email"/>
<button #click.stop="step++">next</button>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
step: takeState('wizard.step')
.commit('setWizardStep')
.map(),
...takeState('user')
.expose(['name', 'email'])
.commit('editUser')
.map()
}
}
</script>
store/index.js
export default new Vuex.Store({
state: {
wizard: {
step: 1
},
user: {
name: 'John',
email: 'john#doe.com'
}
},
mutations: {
setWizardStep(state, step) {
state.wizard.step = step;
},
editUser(state, patch) {
state.user = Object.assign({}, state.user, patch);
}
}
});
Using: vuex-map-fields
from vuex-map-fields repo:
Enable two-way data binding for form fields saved in a Vuex store.
Vuex Store
import Vue from 'vue'
import Vuex from 'vuex'
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex)
export const store = new Vuex.Store({
strict: true,
state: {
counter: 0,
vuex_state: 'starting string'
},
getters: {
getField, // Add the `getField` getter to the `getters` of your Vuex store.
vuex_getter1: (state) => {
return state.vuex_string
}
}
mutations: {
updateField, // Add the `getField` getter to the `getters` of your Vuex store.
}
})
Component code:
<template>
<div>
<div>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="internal_state"></textarea>
<p> {{ internal_state }}</p>
<hr>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="vuex_state"></textarea>
<p> {{ vuex_state }}</p>
<hr>
</div>
</div>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
name: 'WriteArea',
data () {
return {
internal_state: ''
}
},
computed: {
// The `mapFields` function takes an array of
// field names and generates corresponding
// computed properties with getter and setter
// functions for accessing the Vuex store.
...mapFields([
'vuex_state ',
]),
},
}
</script>

Categories

Resources