Switch themes like a fox 🦊 based on Ambient Light 💡 in your Angular Apps
Tags: angular, typescript, javascript, webdev
Published: 2020-03-23 08:11:41

> **DISCLAIMER:** This is a continuation of this article where I write about implementing a theme switch in an Angular App. If you haven't already read that, I recommend you to read this first.👇🏻

{% link https://dev.to/siddajmera/angular-apps-with-a-theme-switch-like-the-ngular-material-site-darkmode-3mno %}

> That way you'll have more context of where I'm coming from.🙂

> This article is intended for an audience with mixed experience levels. I've still added a [TL;DR;](#tldr) below 👇🏻 as a precautionary measure, in case you're just interested in a specific section.

## TL;DR; <a name="tldr"></a>

* [Why Dark Theme in the Dark? 🤷🏻‍♂️](#why-dark-theme-in-the-dark)
* [Determining when it's dark⚡](#determining-when-its-dark)
* [Enter: the `AmbientLightSensor` Web Interface 📡](#enter-ambient-light-sensor-web-interface)
  * [Feature Detection 🕵🏻‍♂️](#feature-detection)
  * [Handling Edge Cases](#handling-edge-cases)
* [Using the `AmbientLightSensor` Web Interface](#using-the-ambient-light-sensor-web-interface)
* [Trying it out 🧪](#lets-try-it-out)
* [Next Steps 👣](#next-steps)
* [Closing Notes 🎉](#closing-notes)

To start, we'll just continue with [the same Angular App](https://stackblitz.com/edit/angular-material-theme-switch) that we built in the [last article](https://dev.to/angular/angular-apps-with-a-theme-switch-like-the-ngular-material-site-darkmode-3mno).

{% stackblitz angular-material-theme-switch %}

## Why Dark Theme in the Dark? 🤷🏻‍♂️ <a name="why-dark-theme-in-the-dark"></a>

So, as I mentioned in the [previous article](https://dev.to/siddajmera/angular-apps-with-a-theme-switch-like-the-ngular-material-site-darkmode-3mno), Dark Mode is awesome and low-lit ambiances are the best suited for #DarkMode.

Now changing themes in our App based on user interaction is OK. But we can take it to the next level by intelligently switching themes, based on the ambient lighting conditions that the user is in.

And that's exactly what we're going to do in this article.

## Determining when it's dark ⚡ <a name="determining-when-its-dark"></a>

Now you might think, how exactly do we determine that it's dark. Well, there's something called **Illuminance** that can help us do that.

> According to [Wikipedia](https://en.wikipedia.org/wiki/Lux#Illuminance):

> Illuminance is a measure of the [luminous flux](https://en.wikipedia.org/wiki/Luminous_flux) spread over a given area.

> One can think of luminous flux (which is measured in lumens BTW) as a measure of the total "amount" of visible light present, and the illuminance as a measure of the intensity of illumination on a surface.

*"So in simpler terms, the luminous flux is inversely proportional to darkness."*

For some reference, we'll use this table to determine the darkness:

{% gist https://gist.github.com/SiddAjmera/81c52ebf3cbefea8bce757573353afdb %}

Judging from the table above, it would be safe to consider that if the luminous flux is 10 or less, we're in a dark environment. But that is just a number that I've chosen. Feel free to chose a number between 10 - 20(or 50 if you like) based on your preference.

Okay, so we can determine whether the environments are light or dark based on the luminous flux. But how do we determine luminous flux? 🤔

![But how?](https://media.giphy.com/media/H6JdkRnhXQaImiCYp2/giphy.gif)

## Enter: the `AmbientLightSensor` Web Interface 📡 <a name="enter-ambient-light-sensor-web-interface"></a>

> This is a cool new interface from the [Sensor APIs](https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs) that returns the current light level or illuminance of the ambient light around the hosting device.

> The `AmbientLightSensor` object has a property named `illuminance` on it, that returns the current light level in [lux](https://en.wikipedia.org/wiki/Lux) of the ambient light level around the hosting device.

It would only work on devices that have the ambient light sensor(hardware) on them(obviously). With the help of this `AmbientLightSensor` interface, our browsers can access the data collected by Ambient Light Sensors on devices. Cool, isn't it? 🤩

> What does that mean for us? Well, you can think of `illuminance` as the luminous flux incident on a surface.

Now we know how to get the `illuminance`, and from the **[Illuminance Table](https://gist.github.com/SiddAjmera/81c52ebf3cbefea8bce757573353afdb)** we can determine whether the environment we're in is dark or light.

So, we'll consider the environment to be dark if `illuminance` <= 10(again, this number is totally up to you), light otherwise.

Using the `AmbientLightSensor` interface to access `illuminance` is pretty straightforward and the usage is mentioned in [this example](https://developer.mozilla.org/en-US/docs/Web/API/AmbientLightSensor#Example) on the MDN Docs.

But there are a lot of other things that we need to take care of while using this interface. Let's go through them one by one.

### Feature Detection 🕵🏻‍♂️: <a name="feature-detection"></a>

This is to determine whether the browser that is running our App has the `AmbientLightSensor` feature on it or not. To detect this, we can simply check:

```js
if ("AmbientLightSensor" in window) {
    // Yay! The Browser has what it takes
}
```

### Handling Edge Cases: <a name="handling-edge-cases"></a>

Checking whether the browser supports a feature doesn't guarantee that everything will work as expected. There might be errors:

* While instantiation the sensor.
* While using it. 
* When the user's permissions might be required to use the sensor.
* When the sensor type might not be supported by the device.

So all these scenarios would result in an error. So while using this interface, we'll have to cater to all these edge cases as well.

Now that we know what we're looking at, let's try to implement this in our App.

## Using the `AmbientLightSensor` Web Interface <a name="using-the-ambient-light-sensor-web-interface"></a>

Reading the `illuminance` and handling all these edge cases is the major task that we should delegate to a service. So let's implement a service in Angular that will handle all these things for us.

The only output that we're going to expect from this service is an Observable that either gives us the `illuminance` or an error message that we could show to the user. So let's do this. I'll name this service `AmbientLightSensorService`. 

Also, since this service would also rely on the `window` object, let's provide it as a value so that we could then inject it as a dependency in our `AmbientLightSensorService`.

So in our `AppModule`:

#### `app.module.ts`
```ts
...
import { AmbientLightSensorService } from "./ambient-light-sensor.service";

@NgModule({
  ...
  providers: [
    AmbientLightSensorService,
    {
      provide: Window,
      useValue: window,
    },
    ...
  ]
})
export class AppModule {}
```
There are also a lot of messages, error types, sensor policy and sensor name etc. that we are going to deal with. So let's also expose them as constants:

#### `common.const.ts`
```ts
export const SENSOR_NAME = "AmbientLightSensor";
export const SENSOR_POLICY_NAME = "ambient-light-sensor";
export const ACCESS_DENIED = "denied";
export const THEME_OPTIONS_URL = "/assets/options.json";
export const THEME_BASE_PATH = "node_modules/@angular/material/prebuilt-themes";
export const STYLE_TO_SET = "theme";
export const DARK_THEME = "pink-bluegrey";
export const LIGHT_THEME = "deeppurple-amber";

export const ERROR_TYPES = {
  SECURITY: "SecurityError",
  REFERENCE: "ReferenceError",
  NOT_ALLOWED: "NotAllowedError",
  NOT_READABLE: "NotReadableError"
};

export const ERROR_MESSAGES = {
  UNSUPPORTED_FEATURE: "Your browser doesn't support this feature",
  BLOCKED_BY_FEATURE_POLICY:
    "Sensor construction was blocked by a feature policy.",
  NOT_SUPPORTED_BY_USER_AGENT: "Sensor is not supported by the User-Agent.",
  PREMISSION_DENIED: "Permission to use the ambient light sensor is denied.",
  CANNOT_CONNECT: "Cannot connect to the sensor."
};
```
Hopefully, I've named these variables in a way that they are self-explanatory.

Now let's implement this service:

#### `ambient-light-sensor.service.ts`
```ts
import { ReplaySubject, Observable } from "rxjs";
import { Injectable } from "@angular/core";

import {
  SENSOR_NAME,
  SENSOR_POLICY_NAME,
  ACCESS_DENIED,
  ERROR_TYPES,
  ERROR_MESSAGES
} from "./common.const";

@Injectable()
export class AmbientLightSensorService {
  private illuminance: ReplaySubject <number> = new ReplaySubject <number>(1);
  illuminance$: Observable<number> = this.illuminance.asObservable();

  constructor(private window: Window) {
    try {
      if (SENSOR_NAME in window) {
        this.startReading();
      } else {
        this.illuminance.error(ERROR_MESSAGES.UNSUPPORTED_FEATURE);
      }
    } catch (error) {
      // Handle construction errors.
      if (error.name === ERROR_TYPES.SECURITY) {
        this.illuminance.error(ERROR_MESSAGES.BLOCKED_BY_FEATURE_POLICY);
      } else if (error.name === ERROR_TYPES.REFERENCE) {
        this.illuminance.error(ERROR_MESSAGES.NOT_SUPPORTED_BY_USER_AGENT);
      } else {
        this.illuminance.error(`${error.name}: ${error.message}`);
      }
    }
  }

  private startReading() {
    const sensor = new AmbientLightSensor();
    sensor.onreading = () => this.illuminance.next(sensor.illuminance);
    sensor.onerror = async event => {
      // Handle runtime errors.
      if (event.error.name === ERROR_TYPES.NOT_ALLOWED) {
        // Branch to code for requesting permission.
        const result = await navigator.permissions.query({
          name: SENSOR_POLICY_NAME
        });
        if (result.state === ACCESS_DENIED) {
          this.illuminance.error(ERROR_MESSAGES.PREMISSION_DENIED);
          return;
        }
        this.startReading();
      } else if (event.error.name === ERROR_TYPES.NOT_READABLE) {
        this.illuminance.error(ERROR_MESSAGES.CANNOT_CONNECT);
      }
    };
    sensor.start();
  }
}

```
The implementation caters to every edge case that we discussed in the previous section.

Basically, we've exposed the `illuminance` `ReplaySubject<number>` as the `illuminance$` `Observable<number>`.

"Why a `ReplaySubject<number>(1)`?" you might ask. Well, because we don't have an initial value and so it would make more sense to use that instead of using `BehaviorSubject<number>(null)`.

Now, we're pushing a new lux values down the `illuminance` `ReplaySubject` by calling the `next` method on it. And for the error cases, we're pushing out an error using the `error` method.

The method names and error message names are pretty self-explanatory as well. If something is still not clear, please comment down below so that I can elaborate more on it.

And so now that the service is ready, we can inject this service as a dependency in our `HeaderComponent` and leverage the `illuminance$` `Observable` to get access to the lux value(or the error message).

#### `header.component.ts`

```ts
import { Component, OnDestroy, OnInit } from "@angular/core";
import { MatSnackBar } from "@angular/material/snack-bar";
import { Observable, Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";

import { AmbientLightSensorService } from "../ambient-light-sensor.service";
import { DARK_THEME, LIGHT_THEME } from "../common.const";
import { Option } from "../option.model";
import { ThemeService } from "../theme.service";

@Component({
  selector: "app-header",
  templateUrl: "./header.component.html",
  styleUrls: ["./header.component.css"]
})
export class HeaderComponent implements OnInit, OnDestroy {
  options$: Observable<Array<Option>> = this.themeService.getThemeOptions();
  private unsubscribe$ = new Subject<void>();

  constructor(
    private readonly themeService: ThemeService,
    private readonly alsService: AmbientLightSensorService,
    private readonly snackBar: MatSnackBar
  ) {}

  ngOnInit() {
    this.themeService.setTheme(DARK_THEME);
    this.alsService.illuminance$
      .pipe(takeUntil(this.unsubscribe$))
      .subscribe(
        illuminance => {
          illuminance <= 10
            ? this.themeService.setTheme(DARK_THEME)
            : this.themeService.setTheme(LIGHT_THEME);
        },
        error => this.showMessage(error)
      );
  }

  themeChangeHandler(themeToSet) {
    this.themeService.setTheme(themeToSet);
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }

  private showMessage(messageToShow) {
    this.snackBar.open(messageToShow, "OK", {
      duration: 4000
    });
  }
}
```
So as you can notice:

* We've now injected the `AmbientLightSensorService` as a dependency.
* In the `ngOnInit` lifecycle hook, we're `subscribe`ing to the `Observable`. From here:
    * The *success callback* gets called with the `illuminance` value. Here we check the `illuminance` value:
        * If it is `<= 10`, then we set the `DARK_THEME`.
        * If it is `> 10`, then we set the `LIGHT_THEME`.
    * The *error callback* gets called with the `error` message. From there, we're simply calling the `showMessage` method to show a snack bar.

Also, since we're `subscribe`ing to the `Observable` this time around, we'll also have to explicitly do something to avoid any memory leaks. To do it, we go declarative by using the `takeUntil` operator.

> Read more about this approach in [this article](https://medium.com/angular-in-depth/the-best-way-to-unsubscribe-rxjs-observable-in-the-angular-applications-d8f9aa42f6a0) on [AngularInDepth](https://medium.com/angular-in-depth) by [Tomas Trajan](https://medium.com/@tomastrajan)

And that's it. Our `AmbientLightSensor` Theme Switch is now ready. Let's test it out.

## Trying it out 🧪 <a name="lets-try-it-out"></a>

Before we do that, there's a caveat. And it has something to do with browser support.

<p class="ciu_embed" data-feature="ambient-light" data-periods="future_1,current,past_1,past_2" data-accessible-colours="false">
<a href="http://caniuse.com/#feat=ambient-light">
<picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/image/ambient-light.webp">
<img src="https://caniuse.bitsofco.de/image/ambient-light.png" alt="Data on support for the ambient-light feature across the major browsers from caniuse.com">
</picture>
</a>
</p>

As you can see above, browser support isn't that great at the moment. But we'll at least test this on the best browser in the world(ahem Chrome ahem).

To do that, we'll first have to enable a flag:

So I'll navigate to `chrome://flags/#enable-generic-sensor-extra-classes` and enable it on my phone(my laptop doesn't have Ambient Light Sensor on it). And then I'll restart the browser on my phone.

Let's now test this thing out:

{% youtube CEQTOn1OILo %}

![Yay! It worked!](https://media.giphy.com/media/PoImMjCPa8QaiBWJd0/giphy.gif)

And here's the final code:

{% stackblitz ambient-light-sensor-theme-switch %}

## Next Steps 👣 <a name="next-steps"></a>

As of now, there's a slight issue in the App. What if the user doesn't want to change the theme automagically, based on the lighting conditions? We can add in a simple fix as a settings/preference menu asking to turn this behavior ON/OFF and switch the theme only when the behavior is turned ON.

Give it a try and implement the Preference/Settings Menu and then only switch the theme if the user has switched this auto-theme-switch behavior on.

## Closing Notes 🎉 <a name="#closing-notes"></a>

Awwww! You're still here? Thanks for sticking around. I hope you liked it.

I’m extremely grateful to [Martina Kraus](https://dev.to/martinakraus11), and [Rajat Badjatya](https://dev.to/rajatbadjatya) for taking the time to proofread it and providing all the constructive feedback in making this article better.

I hope this article has taught you something new related to Angular and Web in general. If it did, hit that 🧡/🦄 icon, and add it to your reading list(🔖). Also, share this article with your friends who are new to Angular/Web and want to achieve something similar.

Please stay tuned while I work on uploading a video version of this article. 📺

Icon Courtesy: [Angular Material]((https://angular.io/presskit#material)) by AngularIO Press Kit | [CSS](https://thenounproject.com/search/?q=Theme&i=1947501) by monkik from the Noun Project | [Light sensor](https://thenounproject.com/search/?q=light%20sensor&i=1692409) by Adnen Kadri from the Noun Project

Until next time then. 👋🏻