RxJS in Angular: Creating a Weather App

13 min read


RxJS in Angular can seem a bit difficult at first. But once you learn to leverage its power, it makes your code cleaner and more readable!

In this article, I'm going to walk you through developing a simple weather app in Angular and in the process (hopefully!) give you an overview of the most commonly used RxJS features.

The final result is shown below.

Final weather app

Our final weather app using RxJS

Haven't heard about RxJS? It is a Javascript library used to handle asynchronous events and flows emanating from them. Think network calls, notifications and user events. All of these can happen at different times in our apps and RxJS provides us with tools to handle these observable streams, compose them, mix and match to give us the results that we want!

Read more about them here.

Ok, let's kick off our project!

Preparing the project

For the weather app, we just need a simple Angular project, with the Angular Material components library included. This can be achieved by running these commands.

ng new weather-app
ng add @angular/material

The layout of the app is quite simple. We add a material select component in the app.component.html file. Then, we have its corresponding code representing names of cities in the app.component.ts file.

<mat-toolbar color="primary"> My Weather App </mat-toolbar>
<div class="content">
  <mat-form-field>
    <mat-label>Select city</mat-label>
    <mat-select [formControl]="cityControl">
      <mat-option *ngFor="let city of cities" [value]="city">
        {{city}}
      </mat-option>
    </mat-select>
  </mat-form-field>
  <br />
  <router-outlet></router-outlet>
</div>
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit, OnDestroy {
  cities = ["London", "Paris", "Moscow", "New York", "Karachi", "Sydney"];

  cityControl: FormControl;

  constructor(private router: Router) {}

  ngOnInit() {
    this.cityControl = new FormControl("");
    this.cityControl.valueChanges.subscribe((value) => {
      this.router.navigate([value]);
    });
  }

  ngOnDestroy() {}
}

Simple enough? Our select control takes its option values from a simple string array we define here.

What should happen when a city is selected? Since the reactive forms in Angular provide us an observable out of the box to listen to changes in the select control, we just subscribe to the observable to navigate to a route.

Any observable needs to be subscribed to in order for it to execute. This subscription can happen in code as we're doing here. OR in a better way, on the template using the 'async' pipe. We'll come to that in a bit!

Talking about routes, we've added a router-outlet in the template file. And if we look at our app.module.ts, we define our routes like this.

const routes: Routes = [
  {
    path: "",
    component: WeatherReportComponent,
  },
  {
    path: ":locationName",
    component: WeatherReportComponent,
  },
];

I'll be coming to the Weather Report Component in a bit (since that contains our core functionality). But here all you need to see is the routing configuration that we've setup. The locationName parameter will be used in our weather report component to fetch the weather data.

Basic flow of the app

You might have already figured out by now. But just to be clear, the basic flow will be something like this:

  • User selects the city
  • The app will be routed to the correct route with the city name
  • Our weather report component will use the city name and fetch weather data, displaying it on the card

Open Weather API and the weather service

We'll be using the Open Weather Map API to fetch the data about the current forecast in our city of choice. It is a free API for the most part. More details about the different options for their API are available on their website.

To add better structure to our app, let's encapsulate the API call in a weather service component.

@Injectable({ providedIn: "root" })
export class WeatherService {
  constructor(private http: HttpClient) {}

  getWeatherForCity(city: string): Observable<any> {
    const path = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&APPID=695ed9f29c4599b7544d0db5c211d499`;
    return this.http.get(path);
  }
}

This is quite self explanatory! Our function takes in a city name and just returns an observable from the HttpClient GET call.

Note: This is the second time we're seeing an Observable again. Wherever we have an observable, we've a good opportunity to utilize RxJS.

Our app now looks like this.

The city dropdown control

Looks like the basics are setup! Let's go ahead and add our main functionality which is the weather report component.

Building an RxJS Observable stream

We use operators in RxJS to manipulate or change our observable streams. There are a whole host of them available! But, don't worry. We don't need to know all of them. In practice, only a few will suffice for your needs (you can always catch up on the others as you go along).

Since we already know we're receiving the location name in the form of route parameters, let us start from there.

Note that our routeParams are also an observable!

@Component({
  selector: "app-weather-report",
  templateUrl: "./weather-report.component.html",
  styleUrls: ["./weather-report.component.scss"],
})
export class WeatherReportComponent implements OnInit {
  constructor(
    private weatherService: WeatherService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    // Our route params observable
    this.route.params;
  }
}

Most beginners would subscribe to this observable and try to use the param value. But there is a better way to go about this! Let's first see what we actually want to do.

  1. Get the locationName parameter from the route params
  2. Check if the parameter has some value
  3. Call our weather service function for this value
  4. Store the value somewhere so it can be used in our template

Here is how we can use RxJS for each of them.

  1. Use the map operator to transform the params object into our parameter
  2. Use the filter operator to check if we have a valid value
  3. Use a concatMap operator to append or concatenate our service function (which also is an observable, remember?)
  4. Directly assign this observable chain to a data$ observable which we can then use in our template with the async pipe. Hint: That way, we won't need to subscribe or unsubscribe to anything in our code.

The weather-report.component.ts now looks like this.

@Component({
  selector: "app-weather-report",
  templateUrl: "./weather-report.component.html",
  styleUrls: ["./weather-report.component.scss"],
})
export class WeatherReportComponent implements OnInit {
  data$: Observable<any>;

  constructor(
    private weatherService: WeatherService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.data$ = this.route.params.pipe(
      map((params) => params["locationName"]),
      filter((name) => !!name),
      concatMap((name) => this.weatherService.getWeatherForCity(name))
    );
  }
}

Notice how no callbacks or conditional statements are required! It is all very clean and readable, even for someone looking at the code for the first time.

Common RxJS Operators

Let's go through each operator and what it does quickly.

Map - The map operator is simply used to transform the output from the observable. The function is used to specify the transformation. You can make the transformation as complex or simple as you want.

Filter - The filter operator is used to control the flow of the observable according to the condition specified. So in our case e.g. the locationName parameter needs to be some valid value (for which we use the double !!) before sending it to the API call. If the value is null or empty, it would simply stop the observable execution.

ConcatMap - This operator is called a higher order mapping operator. It allows us to join another observable with the original one in the same sequence. There is a lot more detail about this and similar operators like switchMap, mergeMap etc. at various other resources. For our purposes, any of these will work.

Let's now look at how we've setup the template for our component.

<mat-card class="mat-elevation-z5" *ngIf="data$ | async as data">
  <mat-card-header>
    <mat-card-title>{{data.name}}'s weather</mat-card-title>
    <mat-card-subtitle>Today: {{today | date }}</mat-card-subtitle>
  </mat-card-header>
  <mat-card-content>
    <div class="flex-row">
      <div class="temp">
        <span class="big-text">{{data.main.temp | number:'0.0-0'}} C</span>
        <span>Feels like </span>
        <span>{{data.main.feels_like | number: '0.0-0'}} C</span>
      </div>
      <div class="outlook">
        <span>{{data.weather[0].description | titlecase}}</span>
      </div>
    </div>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button>LIKE</button>
    <button mat-button>SHARE</button>
  </mat-card-actions>
</mat-card>
:host {
  display: flex;
  justify-content: center;
  position: relative;
}

.top-bar {
  position: absolute;
  left: 0;
  top: 0;
}

mat-card {
  width: 250px;
}

mat-card::ng-deep.mat-card-header-text {
  width: 100%;
}

.flex-row {
  display: flex;
  flex-direction: row;

  > div {
    flex: 50%;
    border-radius: 10px;
  }

  > .outlook {
    padding: 10px;
    color: black;
    background: rgba(0, 0, 0, 0.1);
  }

  > .temp {
    background: lightblue;
    padding: 10px;
    color: black;
    margin-right: 10px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }

  .big-text {
    font-size: 30px;
  }

  .caption {
    text-transform: uppercase;
    font-size: 15px;
  }

  .image {
    height: 80px;
  }
}

We're using a material card component to bind different items from the data we get from the API. Notice the use of the data$ variable with the async pipe. The *ngIf ensures that the card won't be shown if there is no data. The alias data is a useful way to ensure the async pipe is only used at one place in the template.

I'm including the CSS file here just for completeness. The styling is pretty standard stuff for those who've done layouts. If you want to know more about this part, see my companion post on how I did it. I'll skip over this for now.

Great! Now we have a working version of our app as shown below.

Basic working app in Angular RxJS

Adding the weather icon with RxJS Map

You might've noticed that the weather icon in the final result is missing. This is because we don't get the url for the icon image in the data. Instead we just get an icon identifier string which can then be used to construct a url.

There are multiple ways to do this. Let's use RxJS again to make things easier for us. Since we get an observable from the weather service, we can add a map transformation within the service, so that we get an image url ready for use in our template. The transformation uses the spread operator to keep everything else the same and just appends an image property with our image url.

getWeatherForCity(city: string): Observable<any> {
    const path = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&APPID=695ed9f29c4599b7544d0db5c211d499`;
    return this.http.get(path).pipe(
      map(data => ({
        ...data,
        image: `http://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`
      })),
    );
  }

Then, we simply add an img tag in our template right below the outlook text.

<div class="outlook">
  <img [src]="data.image" class="image" />
  <span>{{data.weather[0].description | titlecase}}</span>
</div>

And we've a nice looking icon to show the current weather report in a visual form.

Weather icon added

Nice touch!

Adding the loading indicator with RxJS Tap

The Open Weather Map API is quite snappy. However, on very slow connections, we might still need to give a visual indicator to the user till we get the data. Let's add a material progress bar at the top of the card to serve this purpose.

How do we control when to show the loader and when to hide it? We'll keep a boolean in our component and use the tap operator in RxJS to set it at at different times in our observable pipeline.

this.data$ = this.route.params.pipe(
  map((params) => params["locationName"]),
  filter((name) => !!name),
  tap(() => {
    this.loading = true;
  }),
  concatMap((name) => this.weatherService.getWeatherForCity(name)),
  tap(() => {
    this.loading = false;
  })
);

The tap operator is perfect for cases where we need to specify a "side effect" in our observable pipeline. It doesn't change the output or the flow of the observable in any way. So it's well suited for setting status variables like this. The rest of the stream keeps working as it is.

Just to simulate our loading on slower networks, I've added a delay operator to the weather service. It will just add a 500ms delay to the observable.

getWeatherForCity(city: string): Observable<any> {
    const path = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&APPID=695ed9f29c4599b7544d0db5c211d499`;
    return this.http.get(path).pipe(
      map(data => ({
        ...data,
        image: `http://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`
      })),
      delay(500)
    );
  }

Very convenient for testing. Here is how the loading indicator looks like now.

Loader added to RxJS weather app

Bonus: Conditional select fields

Last, but not the least, let's convert our city select control into two selectors: one for country and the other for the city. This is better from a UX perspective, since the user can narrow down the selection by choosing a country first.

How do we achieve this conditional select control? Again, there are multiple ways to do it. Since both our controls expose their values as observables, it is best we look for a way through RxJS. In essence, what we want to do is to link the options for the city select control with the value of the country select control.

Let's first add the country control and change the structure of the data a bit.

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit, OnDestroy {
  countries = [
    {
      name: "United Kingdom",
      cities: ["London", "Warwick", "Birmingham"],
    },
    {
      name: "United States",
      cities: ["New York", "Chicago", "Washington"],
    },
    {
      name: "Australia",
      cities: ["Sydney", "Adelaide", "Melbourne"],
    },
    {
      name: "Pakistan",
      cities: ["Lahore", "Karachi", "Islamabad"],
    },
  ];

  countryControl: FormControl;
  cityControl: FormControl;

  constructor(private router: Router) {}

  ngOnInit() {
    this.cityControl = new FormControl("");
    this.cityControl.valueChanges.subscribe((value) => {
      this.router.navigate([value]);
    });

    this.countryControl = new FormControl("");
  }
}
<div class="content">
  <div class="flex-row">
    <mat-form-field class="mr">
      <mat-label>Select country</mat-label>
      <mat-select [formControl]="countryControl">
        <mat-option *ngFor="let country of countries" [value]="country">
          {{country.name}}
        </mat-option>
      </mat-select>
    </mat-form-field>
    <mat-form-field>
      <mat-label>Select city</mat-label>
      <mat-select [formControl]="cityControl">
        <mat-option *ngFor="let city of ??" [value]="city">
          {{city}}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </div>
</div>

Notice how we don't really know what data to use now to plug in our *ngFor directive for the city select control options. Let's create an Observable called cities$ to refer to this conditional city data. Then we simply need to specify the observable pipeline needed to get this data in the ngOnInit function.

this.cities$ = this.countryControl.valueChanges.pipe(
  map((country) => country.cities)
);

Notice how we're just using the map operator to get the city values from the selected country value. No subscriptions or extra variables required in our code!

To finish up, we just add the missing piece in the template. It is the cities$ observable with the async pipe.

<mat-form-field>
  <mat-label>Select city</mat-label>
  <mat-select [formControl]="cityControl">
    <mat-option *ngFor="let city of cities$ | async" [value]="city">
      {{city}}
    </mat-option>
  </mat-select>
</mat-form-field>

And that's it!

We now have a nice looking conditional select control. If you now select a country, the cities control options will automatically be narrowed down for you. Every other functionality in the app remains the same.

Conditional controls for country and city

Final thoughts

If you've reached this point, Kudos! We just created a simple weather app for ourselves with RxJS in Angular with some bits of technical difficulty. More importantly, we just learnt about how RxJS can be leveraged to its full and how it helps in making our code clean, while developing complex flows easier than it once was!

I hope you go ahead to use the awesome RxJS library in your own projects as well. It just takes some getting used to. Once you start thinking in terms of observables and streams, you'll never want to go back!

The complete code for the app can be found on this github repo.

Thanks for reading!

Bye :)

If you liked this article, you might also like Create a responsive card grid in Angular using Flex Layout!

Support

You may also like...