I admit it. I’m terrible at CSS. I’m especially bad at dealing with responsive issues. But I found a solution that I’m simply more comfortable with.
Angular CDK.
I’m not going to go on about installing things from scratch. Rather, I’m going to explain how it works and how I’ve found the best way to use it may be.
The primary tool is called a BreakpointObserver. This is simply an observable that tracks the browser size and orientation. It’s cool, but it’s also a pain in the ass. It doesn’t just return the answer, it “matches”. So, you give it a layout of height and width and it tells you if that height and width “matches”. Well, that’s cool, but do you want to run a bunch of javascript code every time you load a component? Do you want to try to guess every size and see “so… is this it? no, how about this? no, how about…..”. No, you don’t. Here’s how to do it.
You make a function that uses some built in display structures, try to match them from mobile to big browser, then return the one that fits in an observable.
breakpoint$:Observable<{size:string}>
constructor(private breakpointObserver: BreakpointObserver) { this.breakpoint$ = this.breakpoint(); }
private breakpoint() {
console.log("BREAKPOINT INITIALIZED")
let lrg = this.breakpointObserver.observe([Breakpoints.Web]).pipe(
map(({ matches }) => { if (matches) { return { size: "web" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "web" && !environment.production) console.log('screen is sized for web')
}));
let sml = this.breakpointObserver.observe([Breakpoints.Tablet]).pipe(
map(({ matches }) => { if (matches) { return { size: "tablet" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "tablet" && !environment.production) console.log(obj)
})
)
let mobile = this.breakpointObserver.observe([Breakpoints.Handset]).pipe(
map(({ matches }) => { if (matches) { return { size: "handset" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "handset" && !environment.production) console.log('handset')
})
)
return of(lrg, sml, mobile).pipe(mergeAll(), filter(rets => rets.size != 'unknown'))
}
So, here’s what’s happening:
1. I define a public variable for an observable that is going to hold my screen size. (There’s some more data in there, so you can obviously adjust that.)
2. I pull an observable from the breakpointObserver’s “observe” function and do the match. It either returns the size or unknown. Some predefined settings in the Breakpoints object are Web, Tablet, and Handset. That covers the whole gamut of tiny to huge. There are more specific ones, but these are enough for me.
3. Now, each of these observables lrg, sml, and mobile return property “size” with values:”web, tablet, handset or unknown”. I can stick them all into one big observable using “of”, but then I’d have 3 observables inside an observable – that sucks, so I use “mergeAll()” to instead push them all into one observable with a common result set. So, now I have an observable with an array of 3 {size:something}. The result will actually be 2 “unknowns” and 1 value that’s either “web, tablet or handset”.
Then, in the constructor, I’m setting the public value breakpoint$ to this resulting observable.
Now, I put this all into a service:
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Injectable } from '@angular/core';
import { merge, Observable, of } from 'rxjs';
import { filter, map, mergeAll, tap } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class BreakpointService {
breakpoint$:Observable<{size:string}>
constructor(private breakpointObserver: BreakpointObserver) { this.breakpoint$ = this.breakpoint(); }
private breakpoint() {
console.log("BREAKPOINT INITIALIZED")
let lrg = this.breakpointObserver.observe([Breakpoints.Web]).pipe(
map(({ matches }) => { if (matches) { return { size: "web" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "web" && !environment.production) console.log('screen is sized for web')
}));
let sml = this.breakpointObserver.observe([Breakpoints.Tablet]).pipe(
map(({ matches }) => { if (matches) { return { size: "tablet" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "tablet" && !environment.production) console.log(obj)
})
)
let mobile = this.breakpointObserver.observe([Breakpoints.Handset]).pipe(
map(({ matches }) => { if (matches) { return { size: "handset" } } else { return { size: "unknown" } } }),
tap(obj => {
if (obj.size == "handset" && !environment.production) console.log('handset')
})
)
return of(lrg, sml, mobile).pipe(mergeAll(), filter(rets => rets.size != 'unknown'))
}
}
So now I can add that service to the app.module’s proivders, then inject that service and it’s just a one-and-done. You can subscribe to it anywhere.
Now, on the component:
responsiveClass = 'web';
constructor(bps: BreakpointService, private router: Router) {
bps.breakpoint$
.pipe(
map(retval => { return retval.size }),
)
.subscribe(retval=>{
this.responsiveClass = retval;
});
}
See what I did there? I did a “map” and changed the result object to a simple string, because I really just need the string size. ( I “could” have just done an async call from the HTML, but I may want to play with that variable more, so I just made it a normal variable)
Now look what I can do in HTML:
<div [class]="responsiveClass">
<h1>{{responsiveClass}}</h1>
</div>
And in CSS, I can do whatever the hell I want with those classes. I can make them custom for the page or use global things or use specific IDs. It’s full control.
.web{
height: 100px;width:100px;background-color: red;
}
.tablet{
height: 100px;width:100px;background-color: green;
}
.handset{
height: 100px;width:100px;background-color: blue;
}
So, on that page, I could have simply scripted something so if responsiveClass==’tablet’ then do something that only would be done on a tablet for just one particular div or something. Do you see how powerful that is?
Moreover, if you load it in a service, it’s one-and-done. Then you can subscribe to it wherever you want and it’s ready for you.
Obviously, some of you love writing media queries in CSS and doing tons of stuff there, but I find it super messy if things are specific to a page.
Some References: https://medium.com/@pjlamb12/angular-cdks-breakpointobserver-b75df04a1cc2