ViewChannel flash message tutorial

1. Introduction

We are going to build a flash message system in vanilla JavaScript. The flash message demonstrates the all capabilities of the ViewChannel quite nicely.

We we are using vanilla JavaScript which requires us to write our own DOM manipulation inside of a subscriber. When using a reactive framework such as Svelte or Vue, this is not necessary, as these frameworks handle synchronizing with the DOM for us. See Usage with Frameworks for more information.

To get started with the tutorial open and fork this StackBlitz. By using the sandbox you do not have to worry to much about setting up an entire development environment.

2. Files overview

In the sandbox you will see a couple of files:

  1. flash-message.css it contains all styling, and the animations for the flash messages.
  2. index.html contains the HTML with an example flash message, and buttons, which do nothing now, but which will trigger flash messages at the end of this tutorial. Finally it also includes "uiloos" from the UNPKG cdn.
  3. flash-message.js an empty file in which we will write the code needed to create and display flash messages.
  4. main.js this file contains the (empty for now) event handlers for the trigger buttons.

3. Goals

We want our flash messages to have the following behaviors:

  1. They should leave after a certain amount of time. We will call this auto dismissal.
  2. They should pause the auto dismissal when the user hovers over the flash message so the user has more time to read the message.
  3. The flash message should when clicked be removed, so the user can manually remove a flash message.
  4. The flash messages should have a concept of priority, high priority messages should always be displayed first.

4. Instantiating

The first step is to instantiate the ViewChannel in the flash-messages.js file:

/* 
  Because we use the UNPKG variant the ViewChannel 
  module is available under the "uiloosViewChannel" 
  variable.
*/
export const flashMessageChannel = new window.uiloosViewChannel.ViewChannel(
  {},
  window.uiloosViewChannel.createViewChannelSubscriber({
    // Leaving the subscriber empty for now
  })
);

The idea of the flashMessageChannel is that it will contain all the flash messages. It is recommended to create a ViewChannel for each type of view your application / website will have.

For example: you might have a channel for modals, a channel for flash messages and a channel for confirmation dialogs in your application.

5. Presenting

Lets first alter the subscriber function so it responds to the "PRESENT" event.

export const flashMessageChannel = new window.uiloosViewChannel.ViewChannel(
  {},
  window.uiloosViewChannel.createViewChannelSubscriber({
    
    onPresented(event, viewChannel) {
      console.log(event.view.data);
    }
  })
);

Now in main.js import the flashMessageChannel and change the on click event of the flashInfo button:

import { flashMessageChannel } from '/scripts/flash-message.js';

document.getElementById('flashInfo').onclick = () => {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text: 'Info flash message',
      type: 'info'
    },
  });
};

By calling present() we tell the ViewChannel that a new view has been added, and needs to be displayed.

present() accepts an object as the first parameter, in which you can set a data key. The "data" can contain any value you want, as the ViewChannel leaves it alone.

This allow you to pass any information you would like. In the case of the flash message we pass in the "text" of the message, a unique id, and the type of flash message it is.

The flashMessageChannel does nothing apart from logging the ViewChannelView when the event is "PRESENT"

Lets remedy that situation, but first remove the "dummy" flash message inside of the index.html by removing the following code:

<div class="flash-message flash-message-info">
  <div class="flash-message-row">
    <div class="flash-message-content">
      <span class="flash-message-icon"></span>
      <p>Example flash message</p>
    </div>
    <span class="flash-message-close">𐄂</span>
  </div>

  <div
    class="flash-message-progress flash-message-progress-info"
  ></div>
</div>

In order to show the flash message, we need to insert the DOM we just removed from the index.html on each "PRESENTED" event:

// Get a reference to the flash message container
const flashMessagesContainerEl = document.getElementById(
  "flash-messages-container"
);

export const flashMessageChannel = new window.uiloosViewChannel.ViewChannel(
  {},
  window.uiloosViewChannel.createViewChannelSubscriber({
    onPresented(event, viewChannel) {
      // Extract the view from the event for easier access.
      const view = event.view;

      // I like to give the data a meaningfull name.
      const flashMessage = view.data;

      // Create a div with the generated ID.
      const flashMessageEl = document.createElement("div");
      flashMessageEl.id = flashMessage.id;

      // Set the CSS classes of the div.
      flashMessageEl.className = "flash-message flash-message-info";

      // Set the inner HTML using backticks for easier 
      // templating. Note however  that for security 
      // reasons because we use innerHTML the 
      // `flashMessage.text` should never come from 
      // a user directly!
      flashMessageEl.innerHTML = `
        <div class="flash-message-row">
          <div class="flash-message-content">
            <span class="flash-message-icon">
              ⓘ
            </span>
            <p>${flashMessage.text}</p>
          </div>
          <span class="flash-message-close">𐄂</span>
        </div>

        <div id="${flashMessage.id}-progress" class="flash-message-progress flash-message-progress-info"></div>
      `;

      // Append the flash message to make it visible.
      flashMessagesContainerEl.append(flashMessageEl);
    }
  })
);

Try clicking on the "Info" button it should when clicked show a message.

6. Dismissing

To be able to dismiss the flash message we need to change the onPresented and add a click event listener to the flash message, which on click dismisses the view.

Then we need create an onDismissed subscriber method to handle dismissals:

// Get a reference to the flash message container
const flashMessagesContainerEl = document.getElementById(
  "flash-messages-container"
);

export const flashMessageChannel = new window.uiloosViewChannel.ViewChannel(
  {},
  window.uiloosViewChannel.createViewChannelSubscriber({
    onPresented(event, viewChannel) {
      // Abbreviated same as before

      // Append the flash message to make it visible.
      flashMessagesContainerEl.append(flashMessageEl);
      
      // When the flash message is clicked
      // dismiss it, this will trigger 
      // `onDismissed` to be called.
      flashMessageEl.onclick = () => view.dismiss();
    },

    onDismissed(event) {
      const view = event.view;
      const flashMessage = view.data;

      // Get the flash messages div and remove it.
      const flashMessageEl = document.getElementById(flashMessage.id);
      flashMessageEl.remove();
    }
  })
);

If you click on the flash message it should be removed now.

7. Message types

Flash messages usually have different colors for different types of messages. Red for an error message, yellow for a warning, blue for an info message and green for a success message.

To get the colors working we need to add different CSS classes for based on the type of flash message, in onPresented change the code that creates the flash message element:

flashMessageEl.className = `
  flash-message flash-message-${flashMessage.type}
`;

flashMessageEl.innerHTML = `
  <div class="flash-message-row">
    <div class="flash-message-content">
      <span class="flash-message-icon">
        ⓘ
      </span>
      <p>${flashMessage.text}</p>
    </div>
    <span class="flash-message-close">𐄂</span>
  </div>

  <div 
    id="${flashMessage.id}-progress" 
    class="
      flash-message-progress 
      flash-message-progress-${flashMessage.type}
    "
  >
  </div>
`;

One thing we advise you do is create wrapper functions in order to hide the ViewChannel.

Add the following functions in the flash-message.js file at the bottom:

export function infoFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'info'
    },
    priority: 4
  });
}

export function warningFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'warning'
    },
    priority: 1
  });
}

export function errorFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'error'
    },
    priority: 0,
  });
}

export function successFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'success'
    },
    priority: 2
  });
}

Now change the main.js file to this:

import {
  infoFlashMessage,
  successFlashMessage,
  warningFlashMessage,
  errorFlashMessage
} from '/scripts/flash-message.js';

document.getElementById('flashInfo').onclick = () => {
  infoFlashMessage('Info flash message');
};

document.getElementById('flashSuccess').onclick = () => {
  successFlashMessage('Success flash message');
};

document.getElementById('flashWarning').onclick = () => {
  warningFlashMessage('Warning flash message');
};

document.getElementById('flashError').onclick = () => {
  errorFlashMessage('Error flash message');
};

Now clicking the different buttons should result in different flash messages.

8. Priority

One thing you might have noticed is that each of our flash message creator functions also sets priority. Views with higher priority (the lower the number the bigger the priority) are placed earlier in the ViewChannels views array.

At this moment we are not doing anything with this priority, because we always append to the flashMessagesContainerEl element. To fix this change the append to an insertBefore:

// Insert before the current item holding the
// index, if that index does not exist provide
// `null` so it is appended to the list.
flashMessagesContainerEl.insertBefore(
  flashMessageEl,
  flashMessagesContainerEl.children[view.index] ?? null
);

The trick here is that when the insertBefore is called with null it appends the element.

The flash messages should now respect their priority. Try click the buttons multiple times and see that the error message is always on top.

9. AutoDismiss

We want the flash messages to dismiss automatically after a certain duration, luckily this is just a config change. Change the flash message creator functions to this:

export function infoFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'info'
    },
    priority: 4,
    autoDismiss: {
      duration: 2000,
      result: undefined
    }
  });
}

export function warningFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'warning'
    },
    priority: 1,
    autoDismiss: {
      duration: 3000,
      result: undefined
    }
  });
}

export function errorFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'error'
    },
    priority: 0,
    autoDismiss: {
      duration: 5000,
      result: undefined
    }
  });
}

export function successFlashMessage(text) {
  flashMessageChannel.present({
    data: {
      id: Math.random(),
      text,
      type: 'success'
    },
    priority: 2,
    autoDismiss: {
      duration: 2000,
      result: undefined
    }
  });
}

The flash messages should now remove after the configured duration.

One thing worth discussing is that we set the result to undefined. Each view inside of the ViewChannel will have a result, which is a promise. This result can be anything, a boolean for a confirmation dialog, a string when selecting something from a modal etc etc.

When using autoDismiss you must provide the value, via the result, to resolve the promise with when the auto dismiss is triggered.

In our case flash messages do not really have a result, so setting it to undefined is fine.

10. Exit animation

We already have a nice enter animation for the flash message, but no exit animation. To trigger the exit animation we need to add the CSS class flash-message-exit to the flash message element.

We have a problem however: adding CSS class to an element which is no longer in the DOM / screen is futile, you cannot animate what you cannot see. What we need to do instead is trigger the exit animation, and after the animation is complete only then remove the element from the DOM. Like so:

onDismissed(event) {
  const view = event.view;
  const flash = view.data;

  const flashMessageEl = document.getElementById(flash.id);

  flashMessageEl.classList.add('flash-message-exit');

  flashMessageEl.onanimationend = (event) => {
    if (event.animationName === 'slide-out') {
      flashMessageEl.remove();
    }
  };
}

11. Play & pause

One thing about flash messages being automatically dismissed is that you might not have time to read them. So it would be nice if we paused the auto dismiss when hovering over the flash message. We can do this via play() and pause().

Add the following code below the flashMessageEl.onclick in the handling of the onPresented event:

flashMessageEl.onmouseover = () => view.pause();
flashMessageEl.onmouseleave = () => view.play();

This works but it is missing something, a progress bar animation, to visualize the pause state. Add the following code after the insertBefore:

const progressEl = document.getElementById(`${flashMessage.id}-progress`);
progressEl.style.animation = `progress ${view.autoDismiss.duration}ms ease-out`;

Now all that is left is to pause the animation on hover, add the following two events below the onDismissed:

onAutoDismissPlaying(event) {
  const progressEl = document.getElementById(
    `${event.view.data.id}-progress`
  );
  progressEl.style.animationPlayState = "running";
},

onAutoDismissPaused(event) {
  const progressEl = document.getElementById(
    `${event.view.data.id}-progress`
  );
  progressEl.style.animationPlayState = "paused";
}

The trick here is using animationPlayState to control a CSS animation from within JavaScript.

Now the user also gets a sense for how long the flash message will stay visible.

12. What you have learned

  1. That the subscriber receives all events that take place on the ViewChannel, and that in the subscriber you must sync the DOM with what occurred.
  2. That each ViewChannelView has a priority inside of the ViewChannel and that higher priority views are earlier inside of the views array.
  3. That each ViewChannelView has a result, which is a promise, allowing a view to have some sort of result. For example a boolean value for a confirmation dialog.
  4. That autoDismiss allows us to dismiss views automatically after configured interval.
  5. That we can play and pause the autoDismiss.

13. Further reading

  1. Read through the API of the ViewChannel.
  2. View the API for the ViewChannelView. Most often you will work As it often provides the most convenient API for mutating the ActiveList.
  3. When using vanilla JavaScript you must handle DOM manipulation yourself, contrast this with the examples that use a framework.

14. Full code

For reference here is the full code for this tutorial.

Usage with Frameworks
Learn how to use the ViewChannel in combination with frameworks such as Angular and Vue.