admin管理员组

文章数量:1420929

I have a Vue 3 app using Vuetify. Inside a ponent I'm listening to streamed events. For each new event I want to display a notification fading out after x seconds. I think the Snackbar ponent is the correct one to pick but unfortunately a snackbar list isn't supported ( maybe in the near future )

I started with a NotificationList ponent expecting notifications as a prop and tries to display them in a stacked list

Reproduction link

<template>
  <v-snackbar 
              v-for="[notificationId, notificationMessage] in notifications"
              :ref="snackbarElement => notificationReferences.set(notificationId, snackbarElement)"
              :key="notificationId" 
              :model-value="true"
              location="top right"
              @update:model-value="emit('removeNotification', notificationId)"
  >
    {{ notificationMessage }}
  </v-snackbar>
</template>

<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const props = defineProps<{ notifications: Map<string, string> }>();

const emit = defineEmits<{
  (e: 'removeNotification', id: string): void
}>()

const notificationReferences = ref(new Map<string, unknown>());

watch(props.notifications, () => {
  // clear all references to avoid dead ones
  // notificationReferences.value.clear();
}, { immediate: true });

watchEffect(() => {
  let notificationIndex = 0;
  
  for (const [notificationId, snackbarElement] of notificationReferences.value) {
    // const snackbarElementHeight = ??? // get height from Vuetify ponent
    const marginTop = notificationIndex * 60; // snackbarElementHeight;
    
    // snackbarElement.style.marginTop = marginTop + "px";
    // snackbarElement.style.setProperty('margin-top', marginTop + "px");
    
    notificationIndex++;
  }
});
</script>

So other ponents, e.g. the App ponent can consume it like so

<template>
  <v-app>
    <v-main>
      <v-btn @click="addNotification">New notification</v-btn>
      <notification-list :notifications="notifications" @removeNotification="removeNotification" />
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import NotificationList from './NotificationList.vue'

const notifications = ref<Map<string, string>>(new Map())

function addNotification() {
  const notificationId = self.crypto.randomUUID();
  const notificationMessage = (new Date()).toString();
  
  notifications.value.set(notificationId, notificationMessage);
}

function removeNotification(notificationId) {
  notifications.value.delete(notificationId);
}
</script>

I think the code is almost fine, the only thing missing is the vertical offset, currently all snackbars have the same position.

The problem is that when I try to apply a margin to snackbarElement I get the error

TypeError: Cannot set properties of undefined (setting 'marginTop')

but when I try to debug it, e.g. logging the variable, I get the error

TypeError: Cannot convert object to primitive value

I think this is related to Vuetify because I can't reproduce it with Vue and plain div elements, sample

Do you have any ideas how to fix this? Maybe my approach can be simplified?


Sidenote:

I found so maybe I don't even have to deal with notificationReferences at all but the offset prop had no impact, sample


"Bad" temporary workaround

I could add a puted property calculating a margin for each snackbar, assuming a margin of 60px is enough, so that solution is pretty simple but won't work for different snackbar heights

Solution

I have a Vue 3 app using Vuetify. Inside a ponent I'm listening to streamed events. For each new event I want to display a notification fading out after x seconds. I think the Snackbar ponent is the correct one to pick but unfortunately a snackbar list isn't supported ( maybe in the near future )

I started with a NotificationList ponent expecting notifications as a prop and tries to display them in a stacked list

Reproduction link

<template>
  <v-snackbar 
              v-for="[notificationId, notificationMessage] in notifications"
              :ref="snackbarElement => notificationReferences.set(notificationId, snackbarElement)"
              :key="notificationId" 
              :model-value="true"
              location="top right"
              @update:model-value="emit('removeNotification', notificationId)"
  >
    {{ notificationMessage }}
  </v-snackbar>
</template>

<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const props = defineProps<{ notifications: Map<string, string> }>();

const emit = defineEmits<{
  (e: 'removeNotification', id: string): void
}>()

const notificationReferences = ref(new Map<string, unknown>());

watch(props.notifications, () => {
  // clear all references to avoid dead ones
  // notificationReferences.value.clear();
}, { immediate: true });

watchEffect(() => {
  let notificationIndex = 0;
  
  for (const [notificationId, snackbarElement] of notificationReferences.value) {
    // const snackbarElementHeight = ??? // get height from Vuetify ponent
    const marginTop = notificationIndex * 60; // snackbarElementHeight;
    
    // snackbarElement.style.marginTop = marginTop + "px";
    // snackbarElement.style.setProperty('margin-top', marginTop + "px");
    
    notificationIndex++;
  }
});
</script>

So other ponents, e.g. the App ponent can consume it like so

<template>
  <v-app>
    <v-main>
      <v-btn @click="addNotification">New notification</v-btn>
      <notification-list :notifications="notifications" @removeNotification="removeNotification" />
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import NotificationList from './NotificationList.vue'

const notifications = ref<Map<string, string>>(new Map())

function addNotification() {
  const notificationId = self.crypto.randomUUID();
  const notificationMessage = (new Date()).toString();
  
  notifications.value.set(notificationId, notificationMessage);
}

function removeNotification(notificationId) {
  notifications.value.delete(notificationId);
}
</script>

I think the code is almost fine, the only thing missing is the vertical offset, currently all snackbars have the same position.

The problem is that when I try to apply a margin to snackbarElement I get the error

TypeError: Cannot set properties of undefined (setting 'marginTop')

but when I try to debug it, e.g. logging the variable, I get the error

TypeError: Cannot convert object to primitive value

I think this is related to Vuetify because I can't reproduce it with Vue and plain div elements, sample

Do you have any ideas how to fix this? Maybe my approach can be simplified?


Sidenote:

I found https://vuetifyjs./en/api/v-snackbar/#props-offset so maybe I don't even have to deal with notificationReferences at all but the offset prop had no impact, sample


"Bad" temporary workaround

I could add a puted property calculating a margin for each snackbar, assuming a margin of 60px is enough, so that solution is pretty simple but won't work for different snackbar heights

Solution

Share Improve this question edited Mar 13, 2023 at 15:57 baitendbidz asked Mar 2, 2023 at 14:39 baitendbidzbaitendbidz 8054 gold badges22 silver badges71 bronze badges 4
  • You will have to add this functionality yourself. Fortunately, you are not the first person to want this feature and you can find tutorials like this one showing how you can add it. The tutorial is specifically for Vuetify 2 but I don't believe there's any reason why it won't work the same with Vuetify 3. – yoduh Commented Mar 2, 2023 at 15:06
  • 1 The best library I have ever used for notifications is vue-toastification(github./Maronato/vue-toastification). It is available for Vue3. You can see the demo here(vue-toastification.maronato.dev) and use this instead of manual work in the snack bar. – Neha Soni Commented Mar 2, 2023 at 15:24
  • What if you set a max-height for your snackbar? Then you can set the margin with peace of mind. And you should set the margin to zero after a certain number of adding margins (not to over pass the good height of the page) – EspressoCode Commented Mar 14, 2023 at 9:04
  • @EspressoCode yes, I thought about that. But I think my problem is not related to snackbars anymore... I mean the code is almost fine, only the snackbarElement variable inside watchEffect is struggling... – baitendbidz Commented Mar 14, 2023 at 12:12
Add a ment  | 

2 Answers 2

Reset to default 4

I'm afraid you will not get a canonical answer, as for Material Design specifications:

When multiple snackbar updates are necessary, they should appear one at a time.

[…] Avoid stacking snackbars on top of one another.

If you want to do it anyways, I would remend to use v-alert ponents. You will need a bit of styling, a transition and a timeout to get what you want. Not the perfect solution but, in my opinion, way less hackish than trying to do something similar using snackbars.

<div class="notificationContainer">
  <v-slide-y-transition group>
    <v-alert
      v-for="notification in notifications"
      theme="dark"
    >{{ notification[1] }}</v-alert>
  </v-slide-y-transition>
</div>
<v-btn @click="addNotification">New notification</v-btn>
.notificationContainer {
  position: fixed;
  top: 10px;
  right: 10px;
  display: grid;
  grid-gap: .5em;
  z-index: 99;
}

Here you can see a working example.

use filter method instead of splice

本文标签: javascriptHow to display multiple notifications with VuetifyStack Overflow