In a previous post we have layed the foundation for our custom overlay. To recap, we wanted to build a Google Drive-like custom overlay that looks and feels much like the one built for MachineLabs. Let’s have a look at a preview:
In the first part of this series we learned how to use Angular’s CDK to create our very first custom overlay. On top of that we made it configurable, implemented a “handle” to control (e.g. close) an opened overlay and made it possible to share data with the overlay component.
In this post, we’ll pick up from where we left off and implement a few additional features that will take our overlay to the next level. More specifically, we’ll implement keyboard support, image preloading and add animations in order to make our overlay more engaging and provide better feedback. In the end, we’ll complete this post by adding a toolbar component to fully match Google Drive’s look and feel.
Let’s dive right into it!
TABLE OF CONTENTS
Adding keyboard support
Adding keyboard support is easy. All we need to do is to use the
@HostListener() decorator. This decorator is a function decorator that accepts an event name as an argument. Let’s use it inside our
FilePreviewOverlayComponent to listen to
keydown events on the HTML Document, so that we can close the overlay whenever the escape button was pressed.
Closing the dialog from within the overlay component is only possible because the
FilePreviewOverlayRef is now available via the DI system. Remember that we created our own custom injector and defined custom injection tokens for the remote control and the data that we want to share with the component.
Let’s have a look at the code:
Using the host listener we decorate a class method that is called on every
keydown event. The function itself gets the
KeyboardEvent that we can use to check whether it’s the escape key and only then call
close() on the
That’s it already for adding keyboard support. Go ahead and try it out. Open a file preview and then press the escape button.
Instant app response is without any doubt the best, but there are cases when our apps won’t be able to deliver the content immediately, e.g. slow internet connection or even latency issues. In those cases it’s extremely important to provide users with feedback and indicate that progress is being made. It’s crucial to let the user know what is happening in contrast to keep them guessing. One of the most common forms of such feedback is a progress indicator. It reduces the user’s uncertainty, perception of time and offers a reason to wait.
Looking at our overlay, we are facing the exact same problems. When we click to preview an image, depending on the internet connection, the image is fetched by the browser and progressively rendered onto the screen. If the connection is really bad it may take a while. Also, it doesn’t really look that nice if we display image data as it is received, resulting in a top-down filling in of the image.
To solve this, we can use a progress indicator. The good thing is we don’t need to write one from scratch because Angular Material already provides a nice set of loading indicators, one of which is the
<mat-spinner>. In order to use it, we need to add the
@angular/material to the imports of our application:
Note that the
<mat-spinner> component is an alias for
<mat-progress-spinner mode="indeterminate">. As we can see, the progress-spinner supports different modes, determinate and indeterminate.
The difference is that determinate progress indicators are used to indicate how long an operation will take, whereas indeterminate indicators request that the user needs to wait while something finishes. The latter is used when it’s not necessary to indicate how long it will take or to convey a discrete progress. This is perfect for preloading images because we have no idea how long it may take to fetch the image.
Ok, now that we have added the respective module to our imports we can go ahead and update the template of the
FilePreviewOverlayComponent as well as the component class:
First off, we introduce a new property
loading and initialize it with a meaningful value, e.g.
false. This will show our spinner until the image is loaded. Also note that we are using a property binding to set the
opacity of the
<img> element to 0 when it’s loading and 1 when it’s finished. If we didn’t do this, we’d still see the image being rendered or filled in from top to bottom. This is just a temporary solution that we will replace with a proper solution using Angular’s Animation DSL in just a moment. Last but not least, we define a success callback as a method on our class that is called when the image is loaded. The callback is hooked up in the template via an event binding. In this particular case we are listening for the
load event and when fired, we call the
One more thing to mention is that we needed to wrap the spinner in another element. The reason for this is that we want the spinner to be vertically and horizontally centered. To achieve this we would leverage the CSS
transform property to apply a transformation to the spinner element. The problem is that the
<mat-spinner> component is animated with CSS transforms meaning every transformation we set on the element is overridden. Therefore we use a container that wraps the spinner, so that we can savely apply transformation and center it on the screen.
Here’s the image preloading in action. To better demonstrate the loading, you can throttle your connection in the “Network” tab of Chrome’s DevTools.
Animating the overlay
With animations we aim to guide users between views so they feel comfortable using the site, draw focused-attention to some parts of our application, increase spacial awareness, indicate if data is being loaded, and probably the most important point - smoothly transition users between states.
The problem with our overlay is that it still pops right into our faces. The backdrop is already animated for us, but having an animation only for the backdrop is not enough. We also want to add a little bit of motion to the overlay component, so that it’s less surprising for the user.
Let’s start off by importing the
BrowserAnimationsModule into our application’s
NgModule like this:
With this in place, we can go ahead and define our animations with Angular’s Animation DSL and add it to the component via the
animations metadata property in the
As you can see we created two animations, one for fading in the image (
fade) and the other one to slide up the content (
slideContent). The fade animation will mostly be visible in combination with a spinner. Remember how we used the property binding to temporarily make the image invisible while loading? With the fade animation we can now replace our temporary solution with proper one that leverages the animation DSL.
Next, we define an
animationState property that represents the current animation state, e.g.
leave. By default it’s set to
enter that will cause the content of the file preview to always slide up when it’s opened.
Now we can connect the pieces and set it up in the template:
We can see that the entire content, including the spinner as well as the image, is wrapped in a container
div. That’s because we only want the content to slide up. Later we’ll introduce a toolbar component which comes with its own set of animations. This also implies that it wouldn’t make any sense to use a
@HostBinding() and apply the animation to the host element.
Done! Here’s a live demo with all the code above:
Animations guide our users and the overlay smoothly animates in. This is already much more engaging compared to what we had before but we can do even better. What about closing it? That’s right, we also want it to animate out.
At the moment we simply call
this.overlayRef.dispose() which will detach the portal from the host, eventually removing it from the DOM. With this straightforward approach there’s no way we can execute animations before it is disposed.
What we can do instead is to leverage animation callbacks and introduce event streams that we can subscribe to. Angular provides animation callbacks that are fired when an animation is started and also when it is done.
Let’s start with the animation callbacks and hook them in the template of our
The animation callbacks alone are not very useful, at least not for animating the overlay out. What’s missing is an event bus that we can use to broadcast animation events. That’s needed because when we close an overlay, we have to wait until the leaving animation is done before we can dispose the overlay. We’ll see in just a second how to use this but first let’s define a new
EventEmitter inside the overlay component class:
EventEmitter is an abstraction around a the
Subject type from RxJS.
Cool, now let’s wire up the animation callbacks. To do this, we’ll define the two missing methods
onAnimationDone(). Every time one of the animation callbacks is fired, we broadcast the animation event using
animationStateChanged. Moreover, we need a way to start the
leave animation once we close an overlay. Therefore we’ll also add a method called
startExitAnimation() that sets the
leave. This will then trigger the corresponding animation.
We know that we programatically close an overlay using the remote control aka
FilePreviewOverlayRef. So far we have no access to the overlay component from within the
FilePreviewOverlayRef. To fix this we define a property
componentInstance on the remote control.
We simply set the
componentInstance when an overlay is opened.
Now we can go ahead and introduce two event streams on the
FilePreviewOverlayRef. The first one emits values before the overlay is closed and the other one after is was closed. For the streams we’ll use the
Subject type from RxJS and also expose public methods for each of the event streams.
From there we can use the component instance and subscribe to the animation events and act accordingly. This means that when an animation was started we detach the backdrop and call
_beforeClose. When an animation is finished we broadcast on
_afterClosed. This also means we can dispose the overlay and remove it from the DOM.
That’s it. Here’s a live demo:
Adding a toolbar component
To finish this up we will create a toolbar component, add animations and also make use of the events streams exposed by the remote control to animate the toolbar out before the overlay is closed.
The template is very straightforward and leverages content projection to project content into the template of the toolbar component.
Finally we have to use the toolbar inside the template of the overlay component:
That’s pretty much! Let’s have a look at our final solution:
Angular Master Class at Shopware
Join our upcoming public training!Get a ticket →
Get updates on new articles and trainings.
Join over 1400 other developers who get our content first.
Custom Overlays with Angular's CDK
The Angular Material CDK provides us with tools to build awesome and high-quality Angular components without adopting the Material Design...
Easy Dialogs with Angular Material
Building modals and dialogs isn't easy - if we do it ourselves. Angular Material comes with a powerful dialog service...
Custom themes with Angular Material
Angular Material offers great theming capabilities for both, built-in and custom themes. In this article we'll explore how to make...
RxJS Master Class and courseware updates
If you've been following us for a while, you're quite aware that we're always striving to provide up-to-date and high-quality...
Advanced caching with RxJS
When building web applications, performance should always be a top priority. One very efficient way to optimize the performance of...
A web animations deep dive with Angular
Angular comes with a built-in animation system that lets us create powerful animations based on the Web Animations API. In...