We are gonna see how to leverage the power of directives to create a super simple fullscreen toggle functionality.
Let's see the definition for a directive in Angular:
Directives are classes that add additional behavior to elements in your Angular applications. With Angular's built-in directives, you can manage forms, lists, styles, and what users see.
As the definition says, we can use it to add additional behavior to elements. If it is not cool, I don't know what else is.
Here is a small demo of the directive in action in one of my project (https://cartella.sreyaj.dev)
Directives in Angular
For people often starting with Angular, the term directive and how it works might be confusing. And most of them would simply be stopping at using some of the most common directives like
- NgClass
- NgStyle
- NgIf
- NgFor
We don't really have to care about how adding this would magically make things happen. We might know that to conditionally render an element, we can use *ngIf
and only renders the element if the case is true. That is the end of the story!
One piece of advice that I would like to give to those who are getting the hang of Angular is that try to dig into these features and try to find the underlying implementation.
Angular has really good documentation on different types of directives and how to use them. Read Here:
https://angular.io/guide/built-in-directives#built-in-directives
Tip: To see how the built-in directives are written, just visit their API documentation page and click on the code
icon which will take you to the direct file in the repo:
Creating a Fullscreen Directive
So today we are going to build a custom directive that is gonna help us with one single functionality - maximize and minimize an element.
Enough talking, let's get right into it.
1. Create a module for our directive
This is actually optional. But I do like to create a module and then the directive will be declared and exported from this module. Then I would import this module wherever I need this functionality.
import { NgModule } from "@angular/core";
import { MaximizeDirective } from "./maxmimize.directive";
@NgModule({
declarations: [MaximizeDirective],
exports: [MaximizeDirective] //<-- Make sure to export it
})
export class MaximizeModule {}
2. Create the directive
Next up we create the directive. I am calling it the maximize
directive. Once the directive is created let us add the logic to it.
The idea is to basically make the width and height of the element (where the directive is placed) to say 100vw
and 100vh
so that it spans the entire screen real estate. You might also want to change the positioning of the element in some cases.
import { Directive, ElementRef, Renderer2 } from "@angular/core";
import { BehaviorSubject } from "rxjs";
import { tap } from "rxjs/operators";
@Directive({
selector: "[maximize]",
exportAs: "maximize" // <-- Make not of this here
})
export class MaximizeDirective {
private isMaximizedSubject = new BehaviorSubject(false);
isMaximized$ = this.isMaximizedSubject.asObservable();
constructor(private el: ElementRef, private renderer: Renderer2) {}
toggle() {
this.isMaximizedSubject?.getValue() ? this.minimize() : this.maximize();
}
maximize() {
if (this.el) {
this.isMaximizedSubject.next(true);
this.renderer.addClass(this.el.nativeElement, "fullscreen");
}
}
minimize() {
if (this.el) {
this.isMaximizedSubject.next(false);
this.renderer.removeClass(this.el.nativeElement, "fullscreen");
}
}
}
Let me break down the code for you guys.
First thing is that we need to get hold of the element on which the directive is applied. We do that by injecting ElementRef
in the constructor. This would give reference to the element.
Next, we keep a subject to maintain the state of the directive (to know whether we are currently minimized or maximized).
Modifying the element property
So I have a CSS class defined which just modifies the height and width:
.fullscreen {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
}
The idea now is to toggle this class when the user wants it to be in fullscreen mode or not. For that, we get hold of Renderer2
(Doc) which is a great way to manipulate DOM elements in Angular.
So write two functions one for adding the class and one for removing. We update the state when these functions are called.
Another function called toggle
is added so that the user can perform the operation without having to know about the current state.
Exporting the directive instance
In the section above, I have marked a particular line of code:
@Directive({
selector: "[maximize]",
exportAs: "maximize" // <-- Make not of this here
})
This is a really interesting feature that angular provides. In the directive, we can specify whether we want to make the instance available in the template.
Read here: https://angular.io/api/core/Directive#exportas
What it does is that the directive instance will be made available in the template using the specified name:
<div class="card" maximize #maximize="maximize">
<p>{{(maximize.isMaximized$ | async)}}</p>
</div>
See how I access the property isMaximized$
of the directive in the template. Similarly, all the public properties of the directive can be accessed using maximize
.
Usage
For using the directive, first I import the module in my AppModule
. If you have already declared the directive in your AppModule
, you can skip this step.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
import { MaximizeModule } from "./maximize/maximize.module";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, MaximizeModule], // <-- Imported the module
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
In your template, you can now add the directive to any element
<div class="card" maximize #maximize="maximize">
<button class="min-button"
(click)="maximize.minimize()">
</button>
<button class="max-button"
(click)="maximize.maximize()">
</button>
</div>
So like we've seen in the section where we talked about getting the instance, we can get hold of the minimize()
and maximize()
function in the directive to apply/remove the class respectively.
Bonus - Go Fullscreen in the browser too
So what we did would just maximize the element, but you would also want to make the browser go fullscreen, we can add that too.
- Install the
screenfull
library
npm i screenfull
- Update the directive like so:
...
import * as Fullscreen from "screenfull"; // <-- add the import
...
maximize() {
if (this.el) {
this.isMaximizedSubject.next(true);
this.renderer.addClass(this.el.nativeElement, "fullscreen");
if (Fullscreen.isEnabled) {
Fullscreen.request(); // <-- request fullscreen mode
}
}
}
minimize() {
if (this.el) {
this.isMaximizedSubject.next(false);
this.renderer.removeClass(this.el.nativeElement, "fullscreen");
if (Fullscreen.isEnabled) {
Fullscreen.exit(); // <-- exit fullscreen mode
}
}
}
Note: You might want to open the sandbox preview in a separate window to get the fullscreen working or directly visit this preview URL: https://x6epr.csb.app/
Code
Do add your thoughts in the comments section.
Stay Safe ❤️
Top comments (12)
I'm curious. What's the intent behind using an empty pipe() call in the definition of the isMaximized$ property? I don't think I've often seen code where a call is made to pipe() that doesn't include operators within it. Does it help in the subscription teardown or something similar?
Both ways convert the subject into an observable. So you convert subjects to observables and expose only the observables to make sure consumers cannot call
next()
and emit values.Update: I have changed it to
asObservable
as I don't want to create this impression. UsingasObservable
is probably the better approach.Yeah, I've seen the usage of "asObservable()" more often.
And I guess that's the best approach. So I've updated the code to reflect the same.
Thanks.
Thank you.
Why do you use a Screenfull library and not the API ?
Best regards.
Screenfull is a wrapper for the Fullscreen API. One benefit of it is that I don't have to worry about cross browser support.
Thank you for reply.
Got it.
I gonna implement this functionnality on all my projects :)
Great!
Make sure to listen to the fullscreen change event and update accordingly. This is to handle when the user clicks on Escape key.
Maybe I'll try to add that too in the code snippet.
Good tutorial. I like the simple real world example. Helps understand how to approach something more complex. Thanks. 👍
Thanks for the acknowledgement! I am trying to do exactly the same.
Thank you for a pretty nice tutorial, I liked the simplicity of it.
Thanks...appreciate it!