Skip to content
Advertisement

Vuex Action committing mutation

I have a vue app where a user can randomize a title and subtitle OR edit the fields using a custom input component.

When a user chooses to edit, I’d like to send the updated title and subtitle from the input component to the store to mutate the title and subtitle state when clicking the save button after filling out the values desired in the input component.

Currently able to pass values from parent to child and had an emit present for the parent to listen to, however, I’m not sure how to update the original values to the custom ones and get “undefined” as a result from the $emit.

I can’t seem to find a solution to this problem, all the forums I have been on haven’t helped so I really hope someone on here can help me with my problem; would really appreciate it.

Parent.vue

<template>
  <main class="home-page page">
    <div v-if="!editMode">
      <div>
        <span>Title: </span>{{title}}
      </div>

      <div>
        <span>Subtitle: </span>{{subtitle}}
      </div>

      <div>
        <button @click="randomizeTitleAndSubtitle">
          Randomize
        </button>
        <button @click="onEdit">Edit</button>
      </div>
    </div>

    <div v-else>

      <DoubleInput
        :value="{ title, subtitle }"
      />

      <div>
        <button @click="onCancel">Cancel</button>
        <button @click="onSave">Save</button>
      </div>
    </div>
  </main>
</template>

<script>
// @ is an alias to /src
import DoubleInput from '@/components/DoubleInput.vue';
import { mapState, mapActions } from 'vuex';

export default {
  name: 'Parent',
  components: {
    DoubleInput,
  },
  data() {
    return {
      editMode: false,
    };
  },
  computed: {
    ...mapState(['title', 'subtitle']),
  },
  methods: {
    ...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
    onEdit() {
      this.editMode = true;
    },
    onCancel() {
      this.editMode = false;
    },
    onSave() {
      this.editMode = false;
      const newTitle = this.title;
      const newSubtitle = this.subtitle;
      this.updateTitleAndSubtitle({ newTitle, newSubtitle });
    },
  },
  mounted() {
    this.randomizeTitleAndSubtitle();
  },
};
</script>

Child.vue

<template>
  <div>
    <label>Edit Title: </label>
    <input type="text" ref="title" :value="value.title" @input="updateValue()" />

    <label>Edit Subtitle: </label>
    <input type="text" ref="subtitle" :value="value.subtitle" @input="updateValue()" />

  </div>
</template>

<script>
export default {
  name: 'Child',
  props: ['value'],
  methods: {
    updateValue() {
      this.$emit('input', {
        title: this.$refs.title.value,
        subtitle: this.$refs.subtitle.value,
      });
    },
  },
};
</script>

Store

import Vue from 'vue';
import Vuex from 'vuex';
import randomWords from 'random-words';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    title: '',
    subtitle: '',
  },
  mutations: {
    UPDATE_TITLE(state, value) {
      state.title = value;
    },
    UPDATE_SUBTITLE(state, value) {
      state.subtitle = value;
    },
  },
  actions: {
    randomizeTitle({ commit }) {
      const newTitle = randomWords();
      commit('UPDATE_TITLE', newTitle);
    },
    randomizeSubtitle({ commit }) {
      const newSubtitle = randomWords();
      commit('UPDATE_SUBTITLE', newSubtitle);
    },
    randomizeTitleAndSubtitle({ dispatch }) {
      dispatch('randomizeTitle');
      dispatch('randomizeSubtitle');
    },
    updateTitleAndSubtitle({ commit }, value) {
      const payload = {
        title: value.title || null,
        subtitle: value.subtitle || null,
      };

      commit('UPDATE_TITLE', payload);
      commit('UPDATE_SUBTITLE', payload]);
    },
  },
  modules: {
  },
});

Answer

Where I was having the biggest issue was most in the Vuex store, not the parent to child lifecycle like I thought. The emit was working just fine and needed to add in some computed properties to the custom input component. How I was approaching the store was completely backwards and gutted the updateTitleAndSubtitle() action to what’s shown below. And lastly, added an @input that would send the updated object of values to onEdit() to set the values to an empty object in the data. Then, use that object with the new values to dispatch/commit to the store! Vualá ~ the desired behavior, no errors, and ended up figuring it out with some time.

What I was missing was passing the new emitted data object to a store action to then mutate the state. The whole concept behind this code challenge was to take in data from the store, modify it through a component, send back the modified data to the store to then change the state. A bit overkill for this, BUT it’s the practice and concept I needed to approach a much larger problem in an existing application at work.

Here’s the code breakdown!

Custom Input:

<template>
  <div>
    <label for="title">Edit Title: </label>
    <input
      type="text"
      id="title"
      :setTitle="setTitle"
      ref="title"
      :value="value.title"
      @input="updateValue()"
    />

    <label for="title">Edit Subtitle: </label>
    <input
      type="text"
      id="subtitle"
      :setSubtitle="setSubtitle"
      ref="subtitle"
      :value="value.subtitle"
      @input="updateValue()"
    />

  </div>
</template>

<script>
export default {
  name: 'DoubleInput',
  props: {
    value: {
      type: Object,
      required: true,
    },
  },
  computed: {
    setTitle() {
      // console.log('set title: ', this.value.title);
      return this.value.title;
    },
    setSubtitle() {
      // console.log('set subtitle: ', this.value.subtitle);
      return this.value.subtitle;
    },
  },
  methods: {
    updateValue() {
      this.$emit('input', {
        title: this.$refs.title.value,
        subtitle: this.$refs.subtitle.value,
      });
    },
  },
};
</script>

Parent:

<template>
  <main class="home-page page">

    <!-- <span class="bold">Title:</span> {{ title }} <br>
    <span class="bold">Subtitle:</span> {{ subtitle }}

    <hr> -->

    <div v-if="!editMode" class="display-information">
      <div class="title">
        <span class="bold">Title: </span>{{title}}
      </div>

      <div class="subtitle">
        <span class="bold">Subtitle: </span>{{subtitle}}
      </div>

      <div class="controls">
        <button id="randomize-button" class="control-button" @click="randomizeTitleAndSubtitle">
          Randomize
        </button>
        <button id="edit-button" class="control-button" @click="onEdit">Edit</button>
      </div>
    </div>

    <div v-else class="edit-controls">

      <CustomInput
        :value="{ title, subtitle }"
        @input="v => onEdit(v)"
      />     

      <div class="controls">
        <button id="cancel-button" class="control-button" @click="onCancel">Cancel</button>
        <button id="save-button" class="control-button" @click="onSave(v)">Save</button>
      </div>
    </div>
  </main>
</template>

<script>
// @ is an alias to /src
import CustomInput from '@/components/CustomInput.vue';
import { mapState, mapActions } from 'vuex';

export default {
  name: 'Home',
  components: {
    CustomInput,
  },
  data() {
    return {
      editMode: false,
      v: {},
    };
  },
  computed: {
    ...mapState(['title', 'subtitle']),
  },
  methods: {
    ...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
    onEdit(v) {
      this.editMode = true;
      this.v = v;
      // console.log('returned value object: ', v);
    },
    onCancel() {
      this.editMode = false;
    },
    onSave() {
      this.editMode = false;
      this.$store.dispatch('updateTitleAndSubtitle', this.v);
    },
  },
  mounted() {
    this.randomizeTitleAndSubtitle();
  },
};
</script>

<style lang="stylus" scoped>
.bold
  font-weight bold

.controls
  width 100%
  display flex
  justify-content space-around
  max-width 20rem
  margin-top 2rem
  margin-left auto
  margin-right auto

.control-button
  height 2.5rem
  border-radius 1.25rem
  background-color white
  border 0.125rem solid black
  padding-left 1.25rem
  padding-right 1.25rem

  &:hover
    cursor pointer
    background-color rgba(0, 0, 0, 0.1)
</style>

Store:

import Vue from 'vue';
import Vuex from 'vuex';
import randomWords from 'random-words';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    title: '',
    subtitle: '',
  },
  mutations: {
    UPDATE_TITLE(state, value) {
      state.title = value;
    },
    UPDATE_SUBTITLE(state, value) {
      state.subtitle = value;
    },
  },
  actions: {
    randomizeTitle({ commit }) {
      const newTitle = randomWords();
      commit('UPDATE_TITLE', newTitle);
    },
    randomizeSubtitle({ commit }) {
      const newSubtitle = randomWords();
      commit('UPDATE_SUBTITLE', newSubtitle);
    },
    setTitle({ commit }, value) {
      commit('UPDATE_TITLE', value);
    },
    setSubtitle({ commit }, value) {
      commit('UPDATE_SUBTITLE', value);
    },
    randomizeTitleAndSubtitle({ dispatch }) {
      dispatch('randomizeTitle');
      dispatch('randomizeSubtitle');
    },
    updateTitleAndSubtitle({ dispatch }, value) {
      dispatch('setTitle', value.title);
      dispatch('setSubtitle', value.subtitle);
    },
  },
  modules: {
  },
});

Advertisement