You have probably heared of Angular Material haven’t you? If you haven’t, it’s a library that provides you with high-quality Material Design components for Angular. Material Design itself is a visual design language that aims for consistency of user experience across all platforms and device sizes. That’s cool but what if your company has its own opinions about styles and the overall look and feel of the UI? How do we get the best of Angular Material without adopting the Material Design visual language?
Tada 🎉! That’s where Angular Material’s Component Dev Kit (CDK for short) comes into play. The CDK provides us with tools to build awesome and high-quality Angular components without adopting the Material Design visual language. Its goal is to make our life as developers easier and extract common behaviors and patterns shared between multiple Angular Material components. For instance, the datepicker, snackbar, or tooltip have something in common; they need to dynamically open up some floating panel on the screen. But that’s just the tip of the ice berg. There are many different packages for all sorts of things such as
a11y that helps us improve the accessibility of our UI components. There’s even a
layout package with utilities to build responsive UIs that react to screen-size changes. For a more complete list, please check out the official documentation.
Over at MachineLabs, we thought it would be useful to provide a way to preview generated output files (mostly images), so users don’t have to download it every single time just to take a quick look. So we sat down to build a Google Drive like overlay with the CDK. This post is meant to share our knowledge with the community and to make you comfortable using the CDK for your own purposes.
In this post, we’ll use the CDK to build a Google Drive-like custom overlay that looks and feels much like the one built for MachineLabs. Here’s how it looks like:
TABLE OF CONTENTS
The building blocks
Let’s start simple and work our way up to the final, fully-fledged solution which will have a very similar API as the
MatDialog service provided by Angular Material. It’s not important to know exactly how the
MatDialog works but it’s definitely helpful. If this is new to you, we recommend to check out our post on Easy Dialogs with Angular Material.
Our solution will be a little less flexible but specifically made for showing a file preview inspired by Google Drive. That said, we’d like to have a nice toolbar at the top and the image being rendered in the middle of the screen.
In general, the
MatDialog is great for showing content in a dialog box but as soon as we want a little bit of a custom look and feel, something that does not look like a white box with content inside, we would need to roll our own overlay. Luckily, we can use the
overlay package from the CDK that has most of the core logic for opening floating panels already baked in. More on that in just a second.
Here are the core building blocks of our application:
As we can see, we have two components, one service and a class that represents a remote control to an opened overlay. The
AppComponent is the root (or entry point) of our application. This component contains a toolbar and the list of files that we can preview. In addition, it has access to a
FilePreviewOverlayService which provides us with the core logic for opening an overlay. At the same time it’s an abstraction for some “heavy” lifting that should be implemented in a resuable manner. Don’t be scared, it’s not going to be super heavy and we’ll break it down into comprehensible chunks. Last but not least, there’s a
FilePreviewOverlayRef which, as mentioned, is a handle used to control (e.g. close) a particular overlay.
For the overlay we choose to render a component, so we can attach some logic and also add animations to our overlay to engage our users and make them happy. We call this component
That’s about it. Now that we have the basic structure in place, we’re ready to look at some code.
Note that this post is the first part out of two in which we lay the foundation for our custom overlay. We’ll build on top of this in the next part and add keyboard support, image preloading and animations.
Before we can start implementing the custom overlay we need to install the CDK. Simply run
npm install @angular/cdk and we’re all set!
Our first overlay
MatDialog we know that when we open an overlay we must specify a component type that is then created dynamically at runtime. This means it is not created by using the component tags inside an HTML template. Also, we know that whenever a component is created at runtime, we must add it to our application module’s
Let’s do that and add the
FilePreviewOverlayComponent to the array of
entryComponents. In addition, we need to add the
OverlayModule to the
imports list of the root
From there, creating an overlay is easy. First, we inject the
Overlay service. This service has a
create() function that we need to call in order to create a
PortalHost for our
FilePreviewOverlayComponent. Finally we need to create a
ComponentPortal from this component and attach it to the
PortalHost. Wait, what? Let’s give it a moment and look at some code before taking it apart:
The first step is to create a
PortalHost. We do that by calling
create() on the
Overlay service. This will return an
OverlayRef instance which is basically a remote control for the overlay. One unique attribute of this
OverlayRef is that it’s a
PortalHost, and once created, we can attach or detach
Portals. We can think of a
PortalHost as a placeholder for a component or template. So in our scenario, we are creating a
ComponentPortal that takes a component type as its fist argument. In order to actually display this component we need to attach the portal to the host.
Ok, but where does the overlay get rendered?
Good question. There’s an
OverlayContainer service which creates a container
div under the hood that gets appended to the
body of the HTML Document. There are a few more wrapper elements created but our component eventually ends up in a
div with a class of
cdk-overlay-pane. Here’s what the DOM structure looks like:
Done. That’s all we need to create our very first custom overlay using the CDK. Let’s try it out and see what we got so far:
Our service only exposes one public method
open() that will take care of creating a custom overlay. For now, the service is quite simple but it gets more complicated as we implement a more sophisticated and complete (functional-wise) overlay. Therefore it’s a good idea to extract the common logic into a service to stay DRY. Imagine we would have the same logic defined in each component we want to show an overlay. No good, right?
Now that we have layed the foundation for our custom overlay, let’s take it one step further and improve on what we have so far. Let’s add a backdrop and specify a scroll and position strategy. Don’t worry if it’s unclear what scroll and position strategy is all about. We’ll cover that in a second.
Configuring the overlay
When creating an overlay, we can pass an optional configuration object to
create() to set the desired options, e.g. whether it has backdrop, the position or scroll strategy, width, height and many more. Here’s an example:
First of all, we allow the consumer of our API to override certain options. Therefore, we update the signature for
open() to also take a configuration object. In addition, we define an interface that describes the shape of the configuration from a consumer perspective:
Next, we define some initial values for the config, so that, by default, every overlay has a backdrop alongside a
With that in place, we can define a new method
getOverlayConfig() which takes care of creating a new
OverlayConfig for the custom overlay. Remember, it’s better to break down the logic into smaller parts instead of implementing everything in one giant function. This ensures better maintainability but also readability of our code.
Our method is quite simple. It takes a
FilePreviewDialogConfig and creates a new
OverlayConfig with the values from the given configuration. However, there are two important things to mention. One is the
scrollStrategy and the other one is the
The scroll strategy is a way of defining how our overlay should behave if the user scrolls while the overlay is open. There are several strategies available as part of the CDK, such as
NoopScrollStrategy: does nothing
CloseScrollStrategy: automatically closes the overlay when scrolling
BlockScrollStrategy: blocks page scrolling
RepositionScrollStrategy: will reposition the overlay element on scroll
For our file preview overlay, we are going to use the
BlockScrollStrategy because we don’t want the user to be scrolling in the background while the overlay is open.
scrollStrategy takes a function that returns a scroll strategy. All strategies are provided by the
Overlay service and can be accessed via the
If we don’t specify a strategy explicitly, all overlays will use the
The position strategy allows us to configure how our overlay is positioned on the screen. There are two position strategies available as part of the CDK:
GlobalPositionStrategy: used for overlays that need to be positioned unrelated to other elements on the screen. This strategy is mostly used for modals or root-level notifications.
ConnectedPositionStrategy: used for overlays that are positioned relative to other elements. This is commonly used for menus or tooltips.
We’ll be using the
GlobalPositionStrategy for our overlay because it’s supposed to be positioned globally on screen, unrelated to other elements.
Similar to the
scrollStrategy we can access all position strategies through the
Overlay service like so:
With the configuration in place, we go ahead and define another method
createOverlay() that hides the complexity of creating an overlay with a given configuration:
We now refactor our
open() method to generate a default config and utilize
Here’s what it looks like in action:
Our overlay looks much more like an overlay as we have imagined it in the beginning. The good thing is that most of the heavy lifting is taken care of by the CDK, such as dynamically creating a component, block page scrolling, or positioning.
So far, so good, but we are still missing some very fundamental functionality. We can open an overlay but what about closing it? This it not yet possible, so let’s go ahead and add this feature.
Closing overlays with a remote control
Just like we use remote controls to snap between television channels, we want a remote control to close our overlays. It will provide an API for modifying, closing, and listening to events on the overlay instance. Especially if we want to be able to close the dialog from within the overlay component, and optionally return a value to the consumer.
Our remote control will be a simple class that exposes only one public method -
close(). For now we keep simple and extend it as we introduce more features. Here’s what it looks like:
When implementing the remote control, the only thing we have to make sure is that we need access to the
OverlayRef. It’s a reference to the overlay (portal host) that allows us to detach the portal. Note that, there’s no
@Injectable decorator attached to the class which means that we can’t leverage the DI system for this service. This, however, is no big deal because we will manually create an instance for every overlay and therefore we don’t need to register a provider either. Theoretically, we could open multiple overlays stacked on top of each other where each overlay has its own remote control. The DI system creates singletons by default. That’s not what we want in this case.
What’s left to do is to update our
open() method to create a remote control and return it to the consumer of our API:
Notice how we pass in the
overlayRef when creating a new
FilePreviewOverlayRef? That’s how we get a hold of the
PortalHost inside the remote. Instead of implementing a class that represents a reference to the open overlay, we could have returned the
OverlayRef directly. However, it’s not a good idea to expose lower-level APIs because users could mess with the overlay and detach the backdrop for instance. Also, we need a little bit more logic later on when we introduce animations. A remote control is a good way of limiting the access to the underlying APIs and expose only those that we want to be publicly available.
From a consumer perspective we now get a handle to the overlay that allows us to programatically close it at some point. Let’s go ahead and update
Here’s our code in action. Remember, once we open an overlay it will automatically close after 2 seconds:
Awesome! We are making serious progress and it’s not far until we reach the top of the mountain.
In the previous sections we have mainly improved the overlay under the hood and layed a foundation for upcoming features. In this section we want to focus on improving the overlay’s ergonomics. This means that we want to be able to close the dialog when we click on the backdrop.
Turns out that the backdrop logic is extremely easy with the CDK. All we have to do is to subscribe to a stream that emits a value when the backdrop was clicked:
That’s it! Imagine how much work this would be without the CDK.
From here we could take it one step further and also close the overlay when a user naviagtes back in the browser history. For our application, however, this doesn’t make much sense because we are not using the router and there’s only one page that we render out to the screen. But feel free to give it a shot! Hint: use the
Location service and subscribe to the browser’s
Sharing data with the overlay component
The goal of this post was to implement a generic file preview dialog rather than a static one. At the moment the overlay is quite static and there’s no way we can share data with the overlay component. Sharing data means we want to be able to provide an image that will be available within the component. After all it’s supposed to be a file preview. Therefore, we need to think about how we can share data with the component that is dynamically created.
Luckily, Angular has a hierarchical dependency injection system (DI for short) that we can leverage for our purpose. For more information on Angular’s DI system, check out this post.
In a nutshell, the DI system is flexible enough that we can reconfigure the injectors at any level of the component tree. That said, there is no such thing as the injector. An application may have multiple injectors and each component instance has its own injector. You hear the bells ring? Right, we can create our own custom injector and provide it with a list of custom injection tokens. It sounds more complicated than it actually is.
Turns out, the CDK already has a class
PortalInjector that that we can use to provide custom injection tokens to components inside a portal. This is exactly what we need. Let’s break ground and implement a function
createInjector() that creates a new
PortalInjector and defines a list of custom injection tokens.
In the code above we create a new
WeakMap, set our custom injection tokens that we want to be available (injectable) in the overlay component, and finally instantiate a new
PortalInjector. The important part though is that we also specify a parent injector (first argument) which is mandatory. Also notice the second argument where we pass in our injection tokens.
There are two things that we are providing. The first token is the
FilePreviewDialogRef. Having the remote control at hand, allows the overlay component to close itself. This is very useful because there will definitely be a close button somewhere. The second token is a custom
InjectionToken that stores the data that we want to share with the component.
InjectionToken we create new file
file-preview-overlay.tokens and instantiate a new
Next, let’s update our
FilePreviewDialogConfig so that the user can specify an image that will be used by the overlay component:
For better readability we’ll also refactor our
open() method and create a new
attachDialogContainer() function that now takes care of creating the injector and component portal, as well as attaching the portal to the host.
With that in place, we can now update our
FilePreviewOverlayComponent and inject the tokens that we have defined on a component level with the help of a custom injector.
We can now define data that will be passed to the overlay component and render an image onto the screen. Here’s an example of how we can pass in data:
Finally with a little bit of styling we come much closer to what we’re trying to achieve.
Where to go from here
This is it. Although creating custom overlays is something that’s more or less tricky to do, this task becomes rather easy with UI libraries like Angular Material that provide us with a common set of tools to build awesome and high-quality Angular components. More specifically, by extracting common behaviors and patterns into a so called Component Dev Kit, it becomes extremely easy to build a custom overlay.
Where to go from here? As mentioned in the beginning, this was only part one and we haven’t fully re-built the Google Drive-like file preview yet. In the next post we will build on top of this and implement keyboard support, image preloading and add animations in order to make our overlay more engaging.
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 - Part 2
In this follow-up post we demonstrate how to use Angular's CDK to build a custom overlay that looks and feels...
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...