mobxjs / mobx-vue-lite

Lightweight Vue 3 bindings for MobX based on Composition API.
MIT License
69 stars 3 forks source link

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

Open tpotjj opened 1 year ago

tpotjj commented 1 year ago

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.