Detect Responsive Screen Sizes in Angular
Publikováno: 12.6.2019
Most of the time, we use CSS media queries to handle responsive, screen size changes to layout our content differently. However, there are times where CSS media queries alone isn't sufficient for t...
Most of the time, we use CSS media queries to handle responsive, screen size changes to layout our content differently. However, there are times where CSS media queries alone isn't sufficient for that. We need to handle the responsiveness in our code.
In this article, I would like to share about how to detect responsive breakpoints in Angular, with a twist - we don't maintaining responsive breakpoint sizes in your Typescript code (because responsive breakpoints are already defined in CSS).
We will use Angular with Bootstrap in this example, but it works for any CSS frameworks and classes. Let's start.
What's the Plan
We will be using CSS Classes to determine the current responsive breakpoints. There are 5 breakpoints in Bootstrap CSS. The CSS classes to determine the visibility of each breakpoints is:
- Visible only on xs:
.d-block .d-sm-none
- Visible only on sm:
.d-none .d-sm-block .d-md-none
- Visible only on md:
.d-none .d-md-block .d-lg-none
- Visible only on lg:
.d-none .d-lg-block .d-xl-none
- Visible only on xl:
.d-none .d-xl-block
The CSS display
property will be toggled between none
or block
. We will apply these classes to HTML elements.
Everytime when screen size changes, we will loop and find the HTML element with style display: block
, this is how we will detect the current breakpoint.
Here is the code if you are too excited to see the solution: https://stackblitz.com/edit/angular-size.
The Implementation: Component
Let's create an Angular component size-detector
.
The component HTML template:
<!-- size-detector.component.html -->
<div *ngFor="let s of sizes" class="{{s.css + ' ' + (prefix + s.id) }}">{{s.name}}</div>
The component Typescript code:
// size-detector.component.ts
...
export class SizeDetectorComponent implements AfterViewInit {
prefix = 'is-';
sizes = [
{
id: SCREEN_SIZE.XS, name: 'xs', css: `d-block d-sm-none`
},
{
id: SCREEN_SIZE.SM, name: 'sm', css: `d-none d-sm-block d-md-none`
},
{
id: SCREEN_SIZE.MD, name: 'md', css: `d-none d-md-block d-lg-none`
},
{
id: SCREEN_SIZE.LG, name: 'lg', css: `d-none d-lg-block d-xl-none`
},
{
id: SCREEN_SIZE.XL, name: 'xl', css: `d-none d-xl-block`
},
];
@HostListener("window:resize", [])
private onResize() {
this.detectScreenSize();
}
ngAfterViewInit() {
this.detectScreenSize();
}
private detectScreenSize() {
// we will write this logic later
}
}
After looking at the component code, you might be wondering where is those SCREEN_SIZE.*
value come from. It is an enum. Let's create the screen size enum
(You may create a new file or just place the enum in same component file)
// screen-size.enum.ts
/_ An enum that define all screen sizes the application support _/
export enum SCREEN_SIZE {
XS,
SM,
MD,
LG,
XL
}
Also, remember to add Bootstrap to your project! You may add it via npm or yarn, but in this example, we will use the easier way. Add the cdn link in index.html
.
<!-- index.html -->
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
The code is pretty expressive itself.
- First, we define a list of
sizes
that we support and the CSS classes that used to determine each breakpoints. - In the HTML, we loop through the size list, create
div
element, assign css and display it. Also note that we give eachdiv
an additional unique css classis-<SIZE_ENUM>
. - We have a function
detectScreenSize
. This is where we will write our logic to detect the screen size changes. We will complete that later. - We need to run the logic to everytime when screen size changes. We use the
HostListener
decorator to listen to thewindow resize
event. - We also need to run the logic when we first initialize the application. We need to run it during the
AfterViewInit
component lifecycle hook.
The Implementation: Service & Component
Now we have the component code "almost" ready, let's start implementing our resize service
.
// resize.service.ts
@Injectable()
export class ResizeService {
get onResize$(): Observable<SCREEN_SIZE> {
return this.resizeSubject.asObservable().pipe(distinctUntilChanged());
}
private resizeSubject: Subject<SCREEN_SIZE>;
constructor() {
this.resizeSubject = new Subject();
}
onResize(size: SCREEN_SIZE) {
this.resizeSubject.next(size);
}
}
The resize service code is simple:
- We create a rxjs subject
resizeSubject
. - We have a public method
onResize
that receivesize
as the parameter. It will then push the value to the resize stream. (We will call this method later in oursize-detector
component) - Notice that we use
distinctUntilChanged
operator in the resize observable. We use that to reduce unnecessary notification. For example, when your screen size change from 200px to 300px, it is still consider asxs
size in bootstrap. We don't need to notify in that case. (You can remove the operator if you need) - We export the the resize stream as observable via
onResize$
. Any components, services, directives, etc can then subscribe to this stream to get notify whenever size is changed.
Next, let's go back to our size-detector
component and update the detectScreenSize
logic.
// size-detector.component.ts
...
private detectScreenSize() {
constructor(private elementRef: ElementRef, private resizeSvc: ResizeService) { }
const currentSize = this.sizes.find(x => {
// get the HTML element
const el = this.elementRef.nativeElement.querySelector(`.${this.prefix}${x.id}`);
// check its display property value
const isVisible = window.getComputedStyle(el).display != 'none';
return isVisible;
});
this.resizeSvc.onResize(currentSize.id);
}
...
Let's breakdown and go through the logic together:
- First, we will need to inject the
ElementRef
and our newly createdResizeService
to our component. - Base on our CSS classes, at any point of time, there will be ONLY ONE HTML element visible. We loop through our
sizes
array and find it. - For each size of our
sizes
array, we will use HTML5 element's query selector to find the element by the unique css class we defined earlier onis-<SIZE_ENUM>
. - Once we find the current visible element, we then notify our resize service by calling the
onResize
method.
Using the Service and Component
You may place the size-detector
component under our root component app-component
. For example:
<!-- app.component.html -->
<hello name="{{ name }}"></hello>
<!-- Your size-detector component place here -->
<app-size-detector></app-size-detector>
In this example, I have another hello-component
in the app-component
, but that doesn't matter.
Since I place the component in app-component
, means I can use the ResizeService
everywhere (directives, components, services, etc).
For instance, let's say I want to detect the screen size changes in hello-component
, I can do so by inject the ResizeService
in constructor, then subscribe to the onSizeChange$
observable and do what I need.
// hello.component.ts
@Component({
selector: 'hello',
template: `<h1>Hello {{size}}!</h1>`,
})
export class HelloComponent {
size: SCREEN_SIZE;
constructor(private resizeSvc: ResizeService) {
// subscribe to the size change stream
this.resizeSvc.onResize$.subscribe(x => {
this.size = x;
});
}
}
In the above code, we detect the screen size changes and simply display the current screen size value.
See it in action!
One of the real life use case scenario might be you have accordion on screen. In mobile, you would like to collapse all accordion panels, show only the active one at a time. However, in desktop, you might want to expand all panel.
Summary
This is how we can detect the screen size changes without maintaining the actual breakpoint sizes in our JavaScript code. Here is the code: https://stackblitz.com/edit/angular-size.
If you think of it, it is not very often that the user changes the screen size when browsing the app. You may handle the screen sizes changes application wide (like our example above) or just handle it everytime you need it (per use case / component basis).
Besides that, if you don't mind to duplicate and maintain the breakpoint sizes in JavaScript code, you may remove the component, move the detectScreenSize
into your service and change a bit on the logic. It is not difficult to implement that. (Try it probably?)
That's all. Happy coding!