9

i used snackbar to show success messages in vuejs. i want to make a global custom snackbar component.

<template>
  <div name="snackbars">
    <v-snackbar
      v-model="snackbar"
      :color="color"
      :timeout="timeout"
      :top="'top'"
    >
      {{ text }}

      <template v-slot:action="{ attrs }">
        <v-btn dark text v-bind="attrs" @click="snackbar = false">
          Close
        </v-btn>
      </template>
    </v-snackbar>
  </div>
</template>

<script>
export default {
  props: {
    snackbar: {
      type: Boolean,
      required: true,
    },
    color: {
      type: String,
      required: false,
      default: "success",
    },
    timeout: {
      type: Number,
      required: false,
      default: 3000,
    },
    text: {
      type: String,
      required: true,
    },
  },
};
</script>

then i import this as a component in my every form like this.

<SnackBar :snackbar="snackbar" :color="color" :text="text" />

but my issue is i can't use snackbar as a prop in my child component. it shows me this error.

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "snackbar" 

how can i fix this issue. can anyone help me?

MJ DEV
  • 686
  • 1
  • 11
  • 31

5 Answers5

18

I realize this is old, but thanks to google, I am going to add my solution. I use this, because I don't see the point of using vuex for a snackbar. It's more work then needed.

Create a vue component named vtoast

<template>
  <v-snackbar
      :color="color"
      :timeout="timer"
      v-model="showSnackbar"
      bottom
      right
  >
    <v-icon left>{{icon}}</v-icon>{{message}}
  </v-snackbar>
</template>

<script>
export default {
  name: "vtoast",
  data() {
    return{
      showSnackbar: false,
      message: '',
      color: 'success',
      icon: 'mdi-check',
      timer: 3000
    }
  },
  methods:{
    show(data) {
      this.message = data.message || 'missing "message".'
      this.color = data.color || 'success'
      this.timer = data.timer || 3000
      this.icon = data.icon || 'mdi-check'
      this.showSnackbar = true
    }
  }
}
</script>

Somewhere in the root of your main app, add the following. (I usually put mine in App.vue)

<template>
...
    <!-- toast -->
    <vtoast ref="vtoast"/>
...
</template>

<script>
import vtoast from '@/your/vtoast/directory/vtoast'
export default{
    name: 'App', //or whatever your root is
    components:{
        vtoast
    },
    mounted() {
      this.$root.vtoast = this.$refs.vtoast
    },
}
</script>

And access it like so...

this.$root.vtoast.show()
this.$root.vtoast.show({message: 'Ahoy there!'})
Alan Spurlock
  • 343
  • 2
  • 10
  • Very useful, clean and easy! Thank you! – Igor Aug 11 '21 at 10:42
  • Hii, i used your method but it's given to show error (this.$root.vtoast.show()) – HEMAL Dec 21 '21 at 06:50
  • This method is more efficient and cleaner – Srilal Sachintha Apr 01 '22 at 14:58
  • If someone also got problems like me after add in App: Here is solution mounted() { this.$nextTick(()=>{ this.$root.vtoast = this.$refs.vtoast }); } https://stackoverflow.com/questions/54355375/vue-js-refs-are-undefined-even-though-this-refs-shows-theyre-there – Indy660 Dec 06 '22 at 07:36
11

i found a way to fix my solution using vuex.

    <template>
      <div name="snackbars">
        <v-snackbar v-model="show" :color="color" :timeout="timeout" :top="'top'">
          {{ text }}
    
          <template v-slot:action="{ attrs }">
            <v-btn dark text v-bind="attrs" @click="show = false">
              Close
            </v-btn>
          </template>
        </v-snackbar>
      </div>
    </template>
    
    <script>
    export default {
      created() {
        this.$store.subscribe((mutation, state) => {
          if (mutation.type === "snackbar/SHOW_MESSAGE") {
            this.text = state.snackbar.text;
            this.color = state.snackbar.color;
            this.timeout = state.snackbar.timeout;
            this.show = true;
          }
        });
      },
      data() {
        return {
          show: false,
          color: "",
          text: "",
          timeout: 0,
        };
      },
    };
    </script>

in my vuex module i wrote like this

    export default {
      namespaced: true,
      state: {
        text: "",
        color: "",
        timeout: "",
      },
      mutations: {
        SHOW_MESSAGE(state, payload) {
          state.text = payload.text;
          state.color = payload.color;
          state.timeout = payload.timeout;
        },
      },
      actions: {
        showSnack({ commit }, payload) {
          commit("SHOW_MESSAGE", payload);
        },
      },
    };

then i import snackbar child component into my parent component and send data like this.

    ...mapActions("snackbar", ["showSnack"]),
    saveDetails() {
       this.showSnack({
            text: "Successfully Saved!",
            color: "success",
            timeout: 3500,
          });
     }
Marcello B.
  • 4,177
  • 11
  • 45
  • 65
MJ DEV
  • 686
  • 1
  • 11
  • 31
  • 2
    I think you can remove the action part and just directly do it in mutations since you don't have any side effects there. – wobsoriano Oct 07 '20 at 03:13
3

Another solution is to use a computed value with getter and setter.

Using options api

<template>
  <v-snackbar v-model="show" :color="color">
    {{ message }}
    <template v-slot:action="{ attrs }">
      <v-btn text v-bind="attrs" @click="show = false">Close</v-btn>
    </template>
  </v-snackbar>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters({
      message: 'snackbar/message',
      color: 'snackbar/color'
    }),
    show: {
      get() {
        return this.$store.state.snackbar.show
      },
      set(v) {
        this.$store.commit('snackbar/SET_SHOW', v)
      }
    }
  }
}
</script>

Using composition api plugin

<template>
  <v-snackbar v-model="show" :color="color">
    {{ message }}
    <template v-slot:action="{ attrs }">
      <v-btn text v-bind="attrs" @click="show = false">Close</v-btn>
    </template>
  </v-snackbar>
</template>

<script>
import { defineComponent, computed } from '@vue/composition-api';

export default defineComponent({
  setup(_props, { root }) {
    const show = computed({
      get: () => root.$store.state.snackbar.show,
      set: (v) => root.$store.commit('snackbar/SET_SHOW', v),
    });
    const message = computed(() => root.$store.state.snackbar.message);
    const color = computed(() => root.$store.state.snackbar.color);

    return {
      show,
      message,
      color,
    };
  },
});
</script>

A better implementation using composables here https://gist.github.com/wobsoriano/2f3f0480f24298e150be0c13f93bac20

wobsoriano
  • 12,348
  • 24
  • 92
  • 162
0

You are having a prop and the same in data.

remove snackbar from data() as it is available from prop.

<script>
export default {
  props: {
    snackbar: {
      type: Boolean,
      required: true,
    },
    color: {
      type: String,
      required: false,
      default: "success",
    },
    timeout: {
      type: Number,
      required: false,
      default: 3000,
    },
    text: {
      type: String,
      required: true,
    },
  }
};
</script>
Stark Buttowski
  • 1,799
  • 2
  • 10
  • 21
0

This is what I did with Options API with mere props and events;

Here is the Snackbar.vue component

<template>
  <div class="text-center">
    <v-snackbar
      transition="true"
      bottom
      right
      v-model="show"
      :color="snackbar.color"
      :timeout="snackbar.timeout"
      class="snackbar-shadow"
    >
      <div class="d-flex align-start alert-notify">
        <v-icon size="24" class="text-white mr-5">{{ snackbar.icon }}</v-icon>
        <p class="mb-0">
          <span class="font-size-root font-weight-600">{{
            snackbar.title
          }}</span>
          <br />
          {{ snackbar.message }}
        </p>
      </div>

      <template v-slot:action="{ attrs }">
        <v-btn
          icon
          elevation="0"
          max-width="136"
          :ripple="false"
          height="43"
          class="font-weight-600 text-capitalize py-3 px-6 rounded-sm"
          color="rgba(255,255,255, .85)"
          text
          v-bind="attrs"
          @click="show = false"
        >
          <v-icon size="13">fas fa-times</v-icon>
        </v-btn>
      </template>
    </v-snackbar>
  </div>
</template>

<script>
export default {
  name: "snackbar",
  props: {
    snackbar: Object,
  },
  computed: {
    show: {
      get() {
        return this.snackbar.visible;
      },
      set(value) {
        this.$emit("closeSnackbar", value);
      },
    },
  },
};
</script>

Here is the App.vue component

 <template>
    <!-- Snackbar -->
    <snackbar :snackbar="snackbar" @closeSnackbar="SnackbarClose"></snackbar>
</template>

<script>
export default {
  name: "app",
  data() {
   return {
    snackbar: {
     visible: false,
     timeout: 2000,
     color: "#11cdef",
     title: "Hello",
     message: null,
     icon: "fas fa-bell",
    },
   };
  },
  created: { this.SnackbarShow(); }
  methods: {
    SnackbarShow() {
     this.snackbar.visible = true;
     this.snackbar.message = "Hola! I'm a snackbar";
    },
    SnackbarClose() {
      this.snackbar.visible = false;
    },
  },
};
</script>
farajael
  • 46
  • 5