Ready to get started?

Join Adocasts Plus for $8/mo, or sign into an existing Adocasts Plus account, to get access to all of our lessons.

robot mascot smiling

Creating A Toast Message Manager

@tomgobich
Published by
@tomgobich
In This Lesson

Learn how to implement a user feedback manager in your app using toast messages and vue-sonner. We'll integrate our flash message manager with state provided from AdonisJS' flash messages store to display success and error messages.

Chapters

00:00 - What Is A Toast Message?
01:05 - Installing the Shadcn-Vue Sonner Component
01:40 - Our First Toast Message
04:02 - Flashing Messages from AdonisJS
07:50 - Displaying Flash Messages from AdonisJS
12:40 - Testing Out Our Flash Message Manager

AI Lesson Overview

Join the Discussion 18 comments

Create a free account to join in on the discussion
  1. @memsbdm

    Hey, since vue-sonner updated to V2 we just need to add
    import 'vue-sonner/style.css' in the toast manager component if anyone has an issue with that :)

    2
    1. Responding to memsbdm
      @tomgobich

      Thank you very much for sharing, memsbdm!! 😊

      0
      1. Responding to tomgobich
        @memsbdm

        Hello Tom, is it possible to display the same toast multiple time ? Let say you fail auth, it will only appear once even if we submit multiple failed login forms

        1
        1. Responding to memsbdm
          @tomgobich

          Hi memsbdm! With how we have it, it should display the toast for every failed login attempt. Vue Sonner stacks the messages by default, but you can see each individually if you hover over the stack.

          The key premise of it is that we're watching the effect of our messages prop. When the prop's proxy detects a mutation, our watchEffect is called, which pushes a new toast into Vue Sonner.

          Try adding a console log within the watchEffect so you can see exactly when it is firing and the message(s) the mutation contains.

          
          watchEffect(() => {
            console.log('ToastManager: messages changed', { ...props.messages })
            runToasts({
              exceptions: props.messages.errorsBag,
              success: props.messages.success,
            })
          })
          Copied!
          • inertia
          • components
          • ToastManager.vue

          It should look something like the below:

          0
  2. @davidtazy

    Hello,
    Sorry if you covered this point in another lesson, but:

    Once the POST /register route is fully implemented, should session.flash('success', …) work?

    In my case, it doesn't. When I console.log the shared data for the messages field in config/inertia.ts, it’s empty.

    On the other hand, the session.flash in the POST /login route is working as expected.

    Can you tell me if it should work and if I just have a bug in my codebase? If this is expected behavior, could you give me some hints to understand the use case?

    And thanks for the awesome work. All these lessons have finally convinced me to learn AdonisJS!

    1
    1. Responding to davidtazy
      @tomgobich

      Hi davidtazy!

      Yeah, the toast system should be fully working by the end of this lesson. And, in fact, In lesson 5.2 Logging Out Users, we add in a session.flash('success' Welcome to PlotMyCourse') after successfully registering the user.

      Are you redirecting the user after registering them? Flash messages are made available for the next request and won't be applied to the current, so the redirect does serve a purpose here.

      Here is the final RegisterController's Store method.

      @inject()
      async store({ request, response, session }: HttpContext, webRegister: WebRegister) {
        const data = await request.validateUsing(registerValidator)
      
        // register the user
        await webRegister.handle({ data })
      
        session.flash('success', 'Welcome to PlotMyCourse')
      
        return response.redirect().toRoute('organizations.create')
      }
      Copied!

      If you'd like & are able, feel free to share a link to the repo and I can see if anything stands out.

      Also, that's awesome to hear! I hope you enjoy AdonisJS and thank you for joining Adocasts Plus!! 😁

      1
      1. Responding to tomgobich
        @davidtazy

        Thanks to your reply, I understand why it wasn't working.
        In register.store, I was redirecting to 'home', but I have middleware that redirects to the 'family.show' route when a user connects for the first time to "jail" him until he fill in the minimal required information.

        export default class UserHasFamilyMiddleware {
          async handle({ auth, response }: HttpContext, next: NextFn) {
            if (auth.user && auth.user.hasNoFamily()) {
              return response.redirect().toRoute('family.show')
            }
            return next()
          }
        }
        Copied!

        So I suppose this redirection is consuming the flash message?

        To fix it, register.store redirects to 'family.show' -> my custom middleware does not interfere -> the flash message appears.

        Thank you very much!!!

        1
        1. Responding to davidtazy
          @tomgobich

          Anytime! Awesome, I'm happy to hear you were able to track it down and get it all fixed up! 😊 Another alternative, if applicable to your use-case, is to reflash any messages. This will push the flash messages forward with a secondary redirection.

          Flash → redirect home → reflash & redirect family.show

          export default class UserHasFamilyMiddleware {
            async handle({ auth, response, session }: HttpContext, next: NextFn) {
              if (auth.user && auth.user.hasNoFamily()) {
                session.reflash()
                return response.redirect().toRoute('family.show')
              }
              return next()
            }
          }
          Copied!
          1
          1. Responding to tomgobich
            @davidtazy

            Perfect! I will definitely use the reflash().

            1
  3. @pavlo-sheiman

    How do you handle re-rendering? If you put toast-manager into component tree which will change (i.e. due to navigation), toasts will flash and disappear. How to put it into persistent component tree (i.e. above the page component)?

    1
    1. Responding to pavlo-sheiman
      @pavlo-sheiman

      Nevermind, found a repo, problem solved.

      1
    2. Responding to pavlo-sheiman
      @tomgobich

      Hi Pavlo! In Inertia, the built-in layout layer is positioned above the page component. This means that the instance of the layout component persists across page changes, as long as the page is linked to using Inertia’s Link component.

      Since we're using the built-in layout layer in Inertia & our ToastManager lives within our AppLayout and AuthLayout, we're achieving exactly that. The only exception would be if we switch between those layouts - in which case you could use nested layouts to resolve that if needed.

      0
      1. Responding to tomgobich
        @pavlo-sheiman

        Yeah. In my app I wasn't using persisting layouts and was just wrapping my pages into layout components, so this is where problem was. Checked your code and switched to persistent layouts - so far so good. Thanks for the course and prompt reply!

        p.s. by any chance you have a course on integrating tansatck table (or something similar) with backend pagination, sorting and filtering?

        1
        1. Responding to pavlo-sheiman
          @tomgobich

          Awesome, happy to hear you were able to get it working! Anytime!

          I have a few lessons on pagination (linked below), but nothing to that extent at present, unfortunately. I debated on whether to include a table with pagination, sorting, and filtering in this series but opted against it due to the series already being lengthy. I'm thinking I might do that as an aside series though.

          0
  4. Hi Tom,

    I'm having issues with messages between layouts (I guess). For example, when I logout I cannot see the message I'm puting with ctx.session.flash on my AuthController.

    async logout({ auth, response, session }: HttpContext) {
        await auth.use('web').logout();
    
        session.flash('success', 'You have been logged out');
    
        return response.redirect().toRoute('login.show');
      }
    Copied!

    I'm seeing that the messages are being received on the ToastManager.vue because of some debug I made with console.log. The only solution I found for now is to wrap the runToasts inside a nextTick but I'm not really sure

    watchEffect(() => {
      runToasts({
          exceptions: props.messages.errorsBag,
          success: props.messages.success,
        })
    })
    Copied!

    Thoughts?

    Thank you!

    1
    1. Responding to german-gonzalez
      @tomgobich

      Hi german-gonzalez!

      Do you also have the runToasts call within the onMounted hook? Since the ToastsManager is added via the layout, when you go from a page using the AppLayout to a page using the AuthLayout the AppLayout's ToastManager will be destroyed and a new one will be created and mounted for the AuthLayout. By having runToasts called inside the onMounted hook, we're populating that initial state for our messages when the component is first created and ready.

      With how we have our layouts defined when we traverse between pages using the same layout, like going from the login page to the register page, the same layout instance will be used. Since our ToastsManager is added via the layout, that also means the same ToastManager instance will be used and that is where the watchEffect comes into play. That will watch the reactive values we're using within the hook for changes and rerun when any are detected.

      onMounted(() => {
        runToasts({
          exceptions: props.messages.errorsBag,
          success: props.messages.success,
        })
      })
      
      watchEffect(() => {
        runToasts({
          exceptions: props.messages.errorsBag,
          success: props.messages.success,
        })
      })
      Copied!

      Why does it work when wrapped in $nextTick? The issue is that watchEffect runs immediately to pickup which reactive values to watch for changes. When it runs immediately, its running in the setup stage of our component at which time the component hasn't been mounted, so we don't have a template yet. That results in nothing happening with our toasts. By wrapping it in $nextTick you're deferring when the watchEffect's runToasts call actually run.

      Hope this helps!!

      1
      1. Responding to tomgobich

        I was missing the onMounted hook! Thank you Tom!

        1
        1. Responding to german-gonzalez
          @tomgobich

          Anytime!! 🙂

          0