Skip to content

Vue TransitionGroup + v-if + MobX computed property #132

Open
@tpotjj

Description

@tpotjj

Now, this might not be an all-day case, and maybe I'm doing something wrong on my side, but here is the explanation of the issue:

In my code I have a 'ToastMessageSerive', which is a MobX store that holds some basic information regarding 'Toasts' that I want to display.
In this store, I generate a computed property based on an observable property, so far so good.
Now, if I want to use that computed property on a in order to determine the 'v-if', the transition won't work anymore...

Here is the code:

import { action, observable, computed, makeAutoObservable } from "mobx";
import { ToastMessage } from "~/models/response/ToastMessage";

export class ToastMessageService {
  @observable
  toastMessagesQueue: ToastMessage[] = [];

  @computed
  get toastMessageQueueLength(): number {
    return this.toastMessagesQueue.length
  }

  @computed
  get displayToastMessages(): ToastMessage[] {
    return this.toastMessagesQueue.slice(0, 2);
  }

  @action.bound
  addToast(toastMessage: ToastMessage) {
    this.toastMessagesQueue.push(toastMessage);
    if (this.toastMessagesQueue.length > 2) {
      const lastToastMessage =
        this.toastMessagesQueue[this.toastMessagesQueue.length - 2];
      setTimeout(() => {
        this.removeToast(toastMessage.id);
      }, lastToastMessage.timeout + toastMessage.timeout + 1000);
    } else {
      setTimeout(() => {
        this.removeToast(toastMessage.id);
      }, toastMessage.timeout);
    }
  }

  @action.bound
  removeToast(toastMessageId: number) {
    this.toastMessagesQueue = this.toastMessagesQueue.filter(
      (toastMessage) => toastMessage.id !== toastMessageId
    );
  }
}

export const toastMessageService = new ToastMessageService();
makeAutoObservable(toastMessageService, {}, { autoBind: true });

&&

<template>
  <Observer>
  <!-- Global notification live region, render this permanently at the end of the document -->
  <div
    aria-live="assertive"
    class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6"
  >
    <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
      <!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
      <TransitionGroup
        v-if="toastMessageService.toastMessageQueueLength"
        key="toast-messages"
        enter-active-class="transform ease-out duration-300 transition"
        enter-from-class="translate-x-full opacity-0"
        enter-to-class="translate-x-0 opacity-100"
        leave-active-class="transform ease-in duration-300 transition"
        leave-from-class="translate-x-0 opacity-100"
        leave-to-class="translate-x-full opacity-0"
      >
        <div
          v-for="message in toastMessageService.toastMessagesQueue"
          :key="message.id"
          class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
        >
          <div class="p-4">
            <div class="flex items-start">
              <div class="flex-shrink-0">
                <CheckCircleIcon
                  class="h-6 w-6 text-green-400"
                  aria-hidden="true"
                />
              </div>
              <div class="ml-3 w-0 flex-1 pt-0.5">
                <p class="text-sm font-medium text-gray-900">
                  {{ toastMessageService.displayToastMessages }}
                  <br/>
                  <!-- {{ messages }} -->
                </p>
                <p class="mt-1 text-sm text-gray-500">
                  {{ message.message }}
                </p>
              </div>
              <div class="ml-4 flex flex-shrink-0">
                <button
                  type="button"
                  @click="toastMessageService.removeToast(message.id)"
                  class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
                >
                  <span class="sr-only">Close</span>
                  <XMarkIcon class="h-5 w-5" aria-hidden="true" />
                </button>
              </div>
            </div>
          </div>
        </div>
      </TransitionGroup>
    </div>
  </div>
  </Observer>
</template>

<script setup>
import { Observer } from "mobx-vue-lite";
import { CheckCircleIcon } from "@heroicons/vue/24/outline";
import { XMarkIcon } from "@heroicons/vue/20/solid";
import { toastMessageService } from "~/services/response/ToastMessageService";
import { ToastMessage } from "~/models/response/ToastMessage";

toastMessageService.addToast(
  new ToastMessage({ id: 1, title: "Success", message: "Hoi", timeout: 3000 })
);
toastMessageService.addToast(
  new ToastMessage({ id: 2, title: "Success", message: "Doei", timeout: 3000 })
);
toastMessageService.addToast(
  new ToastMessage({
    id: 3,
    title: "Success",
    message: "Ben ik weer",
    timeout: 6000,
  })
);
toastMessageService.addToast(
  new ToastMessage({ id: 4, title: "Success", message: "Ciao", timeout: 6000 })
);
</script>

If I would determine the transitions 'v-if' this way, it will work (especially the messages computed value):

<script setup>
import { Observer } from "mobx-vue-lite";
import { CheckCircleIcon } from "@heroicons/vue/24/outline";
import { XMarkIcon } from "@heroicons/vue/20/solid";
import { toastMessageService } from "~/services/response/ToastMessageService";
import { ToastMessage } from "~/models/response/ToastMessage";

const messageQueue = ref([])

function removeToast(id) {
  messageQueue.value = messageQueue.value.filter((item) => item.id !== id);
}

function addToast(toastConfig) {
  messageQueue.value.push(toastConfig);
  if (messageQueue.value.length > 2) {
    const lastMessage = messageQueue.value[messageQueue.value.length - 2];
    console.log(lastMessage);
    setTimeout(() => {
      removeToast(toastConfig.id);
    }, lastMessage.timeout + toastConfig.timeout + 1000);
  } else {
    setTimeout(() => {
      removeToast(toastConfig.id);
    }, toastConfig.timeout);
  }
}

const messages = computed(() => {
  return messageQueue.value.slice(0, 2);
});
</script>

Is there something I'm missing?
Or is there something broken?

Btw, I use Nuxt and I have registered: buildModules: ["mobx-vue-lite/nuxt"].

I already used MobX somewhere else for storig values and nothing is wrong in that part of my code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions