Monday, 13 March 2023

How to display multiple notifications with Vuetify?

I have a Vue 3 app using Vuetify. Inside a component 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 component is the correct one to pick but unfortunately a snackbar list isn't support.

I started with a NotificationList component 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)"
  >
    
  </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 component
    const marginTop = notificationIndex * 60; // snackbarElementHeight;
    
    // snackbarElement.style.marginTop = marginTop + "px";
    // snackbarElement.style.setProperty('margin-top', marginTop + "px");
    
    notificationIndex++;
  }
});
</script>

So other components, e.g. the App component 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

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


Sidenote:

I found https://vuetifyjs.com/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



from How to display multiple notifications with Vuetify?

No comments:

Post a Comment