Vuetify: [Feature Request] Snackbar queue

Created on 31 Oct 2017  路  39Comments  路  Source: vuetifyjs/vuetify

New Functionality

Currently it is not easy to handle interaction of multiple snackbar messages. They can overlap and hide each other. Moreover, because they are added as components inside other components, if parent component is removed (for example, because of a route change), snackbar message also disappears or is even not shown.

Improvements

I would propose that there is a global queue of snackbar messages. Maybe by sending a Vue event. Some options could then be to display messages one after the other, or display them stacked, up to some number.

Bugs or Edge Cases it Helps Avoid

Currently it seems one has to manually implement whole logic how to handle interaction between multiple snackbar messages.

EDIT: I have made a NPM package with this functionality @tozd/vue-snackbar-queue.

Framework feature

Most helpful comment

Yet another one: https://github.com/Aymkdn/v-snackbars

This one is different from the others because it will show all the snackbars in a stack way:
Capture

All 39 comments

The reason for not currently supporting multiple snackbar messages is that MD spec specifies that only one snackbar should be displayed at a time.

However, several people have made similar requests previously. And I think there is some merit to at least include basic support for displaying consecutive snackbar messages. Not a fan of providing a global queue in vuetify though.

Will leave this open for discussion.

One could create a component (say SnackDisplayer) and, using Vuex and stores, displaying the snackbar outside of any component (well... except the 1st level component containing the v-app tag).

I tried this with a quite successful result. Here is a fiddle that demonstrate this:
http://jsfiddle.net/13uc6mu5/

With this configuration (check fiddle source), one can call this.$store.commit('msg/set', { message: 'hello' }) in any component and the snack pop up.

One thing that can be improved is indeed the possibility to have consecutive snackbars.

I think it'd make sense to allow the v-snackbar to handle the message queue internally.

The reason for not currently supporting multiple snackbar messages is that MD spec specifies that only one snackbar should be displayed at a time.

So one way could be that they are queued and displayed one after another. But optional stacking would be cool as well. (Or maybe one message, but with a badge in the corner showing that there are many other pending behind this one.)

Is there some other more MD-approved approach to display multiple messages to the user?

The way Google applies the MD specs is closing the opened snackbar/toast when a new one is dispatched. You can see it in the new Google Calendar web version (more info), Angular Material and Angular Material2.

I wrote a little mixin to provide a snackbar queue, it can be easily incorporated in your app: https://codepen.io/jayblanchard/pen/yoxPRY

@Phlow2001, oh so easy. Thanks!

I would love to also have a badge on the side of the snackbar, to show how many pending snackbars are in the queue, but it is not as easy as I expected: #2392

@Phlow2001 But your mixin does not align to the MD specs that @peluprvi mentioned.

The way Google applies the MD specs is closing the opened snackbar/toast when a new one is dispatched.

@pSchaub Based on the code of @Phlow2001, this code should aligns with the MD specs:

addNotification(text) {
    this.notification = false

    setTimeout(() => {
        this.notificationText = text
        this.notification = true
    }, 250)
}

No snackbar queue, a new notification will replace the current one.

Codepen: https://codepen.io/manuel-di-iorio/pen/YeyVMb
MD example: https://material.angular.io/components/snack-bar/examples

I've been experimenting around with a queue system for the dialogs. I also want to spawn them programatically so here is what I have so far. Let it be an inspiration to whatever feature you guys are building, I'm not confident enough in it to PR it.

index.js

import Vue from 'vue'
import Toast from './Toast'

let queue = []
let showing = false

export { Toast }
export default {
  open(params) {
    if (!params.text) return console.error('[toast] no text supplied')
    if (!params.type) params.type = 'info'

    let propsData = {
      title: params.title,
      text: params.text,
      type: params.type
    }

    let defaultOptions = {
      color: params.type || 'info',
      closeable: true,
      autoHeight: true,
      timeout: 1000,
      multiLine: !!params.title || params.text.length > 80
    }

    params.options = Object.assign(defaultOptions, params.options)
    propsData.options = params.options

    // push into queue
    queue.push(propsData)
    processQueue()
  }
}

function processQueue() {
  if (queue.length < 1) return
  if (showing) return

  console.log(queue)

  let nextInLine = queue[0]
  spawn(nextInLine)
  showing = true

  queue.shift()
}

function spawn(propsData) {
  const ToastComponent = Vue.extend(Toast)
  return new ToastComponent({
    el: document.createElement('div'),
    propsData,
    onClose: function() {
      showing = false
      processQueue()
    }
  })
}

toast.vue

<template>
  <v-snackbar v-model="open" v-bind="options">
    <div class="ctn">
      <div class="title mb-2" v-if="title">{{title}}</div>
      <div class="txt">{{text}}</div>
    </div>
    <v-btn v-if="options.closeable" flat icon @click.native="open = false">
      <fai :icon="['fal', 'times']" size="lg"></fai>
    </v-btn>
  </v-snackbar>
</template>

<script>
/* TODO */

export default {
  name: 'toast',
  props: {
    title: String,
    text: String,
    type: String,
    options: Object,
  },
  data() {
    return {
      open: false,
    }
  },
  watch: {
    open: function(val) {
      if (!val) {
        this.close()
      }
    },
  },
  beforeMount() {
    document.querySelector('#app').appendChild(this.$el)
  },
  mounted() {
    this.open = true
  },
  methods: {
    close() {
      if (this.open) this.open = false
      setTimeout(() => {
        this.$options.onClose()
        this.$destroy()
        removeElement(this.$el)
      }, 700) // wait for close animation
    },
  },
}

function removeElement(el) {
  if (typeof el.remove !== 'undefined') {
    el.remove()
  } else {
    el.parentNode.removeChild(el)
  }
}
</script>

main.js

...
import Toast from './components/toast'
Vue.prototype.$toast = Toast
...

Then just use this.$toast.open({text: 'test'}) and it should show a dialog.
Use at your own risk.

Another Mixin that somebody gave me on the discord channel, sorry I don't remember who

export default {
  data: () => ({
    toast: {
      text: 'I am a Snackbar !',
      color: 'success',
      timeout: 5000,
      top: true,
      bottom: false,
      right: false,
      left: false,
      multiline: false
    },
    notificationQueue: [],
    notification: false
  }),
  computed: {
    hasNotificationsPending () {
      return this.notificationQueue.length > 0
    }
  },
  watch: {
    notification () {
      if (!this.notification && this.hasNotificationsPending) {
        this.toast = this.notificationQueue.shift()
        this.$nextTick(() => { this.notification = true })
      }
    }
  },
  methods: {
    addNotification (toast) {
      if (typeof toast !== 'object') return
      this.notificationQueue.push(toast)

      if (!this.notification) {
        this.toast = this.notificationQueue.shift()
        this.notification = true
      }
    },
    makeToast (text, color = 'info', timeout = 6000, top = true, bottom = false, right = false, left = false, multiline = false, vertical = false) {
      return {
        text,
        color,
        timeout,
        top,
        bottom,
        right,
        left,
        multiline,
        vertical
      }
    }
  }
}

Usage:

this.addNotification({text: 'Some Text', color: 'danger', timeout: 5000})

this.addNotification({text: 'Some Text', color: 'danger', timeout: 5000, top: false, bottom: true, multiline: true})

I have used buefy before and it has a neat implementation for this. There is no need for the component. The $snackbar is available on the Vue instance. This saves me a lot of hassle about putting the data into state first and then using it to display the snack bar. They do something like this:

this.$snackbar.open(`Default, positioned bottom-right with a green 'OK' button`)

This can be triggered by any network call, vuex action, etc.

Soruce: https://buefy.github.io/#/documentation/snackbar

@ankitsinghaniyaz my example is inspired by buefy, you can see the same index structure

Agree to @ankitsinghaniyaz buefy has a nice implementation for this

A couple of months ago I added a component to NPM that handles this and more.
https://www.npmjs.com/package/snackbarstack

Here's a demo
https://codepen.io/Flamenco/full/ZoRvLw/

@Flamenco no github repo for it?

@aldarund It's using a few of our internal libraries I can't open source at this moment; The binaries are free to distribute however.

@Flamenco thanks, pretty nice. But hard to customize it. Such as change position, color etc.

@raphaelsoul Some of those are on our wish list in the todo section of NPM. If I can get to it, I will post a github project to collect feature requests.

What kind of API would you suggest for _change position_?

@Flamenco cool demo, thanks for nice job!
Is it not on GihHub? I'd love to fork it and add a stacking feature

@shershen08 This is not open source yet. We have a collection of Vuetify utilities that will most likely be released under a CC license.

I did a more or less simple CSS Hack i want to share if anybody feels like going in the same direction.

WARNING this will brake vuetify default styling and possibility to change positioning the default way

HTLM

<div id="snackbar">
                    <v-snackbar
                            v-for="(snackbar, index) in snackbars"
                            v-model="snackbar.show"
                            :multi-line="true"
                            :right="true"
                            :timeout=6000
                            :top="true"
                            :color="snackbar.color"
                    >
                        {{ snackbar.text }}
                        <v-btn
                                flat
                                @click="hideSnackbar(index)"
                        >
                            <v-icon>close</v-icon>
                        </v-btn>
                    </v-snackbar>
                </div>

SCSS

#snackbar{
        position: fixed;
        top: 0;
        right: 0;
        z-index: 9999999999;
        display: flex;
        width: 100vw;
        flex-direction: column-reverse;
        align-items: flex-end;
        justify-content: space-around;
        div{
            position: relative !important;
            margin-bottom: 8px;
        }
    }

//vuex-store

snackbars : [],

//vuex-mutations

showSnackbar ( state, value) {
            state.snackbars.push( value )
        },
        hideSnackbar ( state, index ) {
            state.snackbars[index].show = false;
        },

Global Access

Vue.mixin( {
    methods : {
        showSnackbar ( color, text  ) {
            this.$store.commit( 'showSnackbar', {
                'show' : true,
                'color' : color,
                'text' : text
            } )
        },
        hideSnackbar ( index ) {
            this.$store.commit( 'hideSnackbar', index )
        }
    }
} )

//Usage in component

this.showSnackbar('info','bla')

any news here if this will become a standard vuetify feature?

I made in meantime: https://www.npmjs.com/package/@tozd/vue-snackbar-queue

I've just added this functionality into my toast component if anyone interested: https://www.npmjs.com/package/vuetify-toast-snackbar

Why does the snackbar not support newlines (neither \n nor <br>)?

I wrote a little mixin to provide a snackbar queue, it can be easily incorporated in your app: https://codepen.io/jayblanchard/pen/yoxPRY

Upgrade to version 2.0.4 and it no longer works.

When you have 1/2 a dozen user libraries for the functionality, it's probably time to re-evaluate if this is a feature that should be included in Vuetify.

@Flamenco A repo would be great! And the API for it should at least match that of the Vuetify one with w/e else you add to it.

We removed or open sourced most of the dependent proprietary libraries from our project at https://www.npmjs.com/package/snackbarstack. We will change the license and contribute the source to Vuetify there is interest.

I wrote a little mixin to provide a snackbar queue, it can be easily incorporated in your app: https://codepen.io/jayblanchard/pen/yoxPRY

Upgrade to version 2.0.4 and it no longer works.

Just add vuetify: new Vuetify(), to Vue component

A couple of months ago I added a component to NPM that handles this and more.
https://www.npmjs.com/package/snackbarstack

Here's a demo
https://codepen.io/Flamenco/full/ZoRvLw/

This installs another vuetify version 馃槄 . vuetify dependency should be a dev/peer dependency instead in your package.json, @Flamenco ?

@nelson6e65 You are correct. It should not be bundled. Vue is also not marked as peer dependency.

I think this should actually be marked as an external in webpack. I added a a project to my github repo and added this as an issue there. Should be fixed in few hours.

https://github.com/Flamenco/snackbar-stack/issues/1

@nelson6e65 The package.json was updated, and I added initial support for Vuetify 2.x

@nelson6e65 The package.json was updated, and I added initial support for Vuetify 2.x

Nice! I'll give a try.

Thanks!

I put together this using Vuex to store messages and queue notifications app-wide.

Please feel free to check it out: https://codesandbox.io/s/queable-snackbars-notification-d5zi6

It may still needs some improvements, but I didn't manage to make a repo yet

Yet another one: https://github.com/Aymkdn/v-snackbars

This one is different from the others because it will show all the snackbars in a stack way:
Capture

For all who want stacking snackbars, the material design specification discourages stacking snackbars. See https://material.io/components/snackbars#behavior

I'm sure it can be added to Vuetify, but it would be departure from following the spec.

Yes, this is why my original issue suggests both showing them one after the other, in a queue, or stacking them. My own plugin does the former to be according to the spec: https://www.npmjs.com/package/@tozd/vue-snackbar-queue

So yea, this issue is not just about stacking, but in general about support for having multiple messages in flight. How is then this shown should probably be configurable.

Try this tutorial which gives you the ability to create stacked notifications with the tiniest overhead possible

Was this page helpful?
0 / 5 - 0 ratings

Related issues

radicaled picture radicaled  路  3Comments

milleraa picture milleraa  路  3Comments

cawa-93 picture cawa-93  路  3Comments

Antway picture Antway  路  3Comments

dohomi picture dohomi  路  3Comments