Click here to Skip to main content
15,867,594 members
Articles / Web Development / ASP.NET / ASP.NET Core

Angular Fundamentals - Building an Angular 7 Application with .NET Core 2.2 (Global Weather) - Part 1

Rate me:
Please Sign up or sign in to vote.
4.94/5 (63 votes)
5 Feb 2020CPOL13 min read 102.9K   2.3K   151   59
Demonstrating a series of Angular fundamentals through an Angular 7 application.
In this article, we build an Angular 7 application with .NET Core 2.2, and introduce Angular fundamentals, like Bootstrapping, NgModules, Reactive Form, Http Client, Observation, Promise and Routing. The goal is to demonstrate a series of Angular fundamentals through an example application.

Image 1

Introduction

This is the first artcile of Global Weather series, we will build an Angular 7 app with .NET Core 2.2.

This basic app has many of the features you'd expect to find in an API-driven application. It allows users to select their location and show the current weather location.

In part 2, we will build a .Net Core API micro service and intergate with Angular app.

In part 3, we will build unit tests for .Net Core micro service and Angular app. 

Setup Angular CLI Environment

Before we begin, let’s go to Angular tutorial to get instructions to setup Angular CLI environment.

Prerequisites

Before you begin, make sure your development environment includes Node.js and an npm package manager.

Angular requires Node.js version 8.x or 10.x.

To check your version, run node -v in a terminal/console window.

To get Node.js, go to Nodes.

npm package manager

Angular, the Angular CLI, and Angular apps depend on features and functionality provided by libraries that are available as npm packages. To download and install npm packages, you must have an npm package manager.

To install the CLI using npm, open a terminal/console window and enter the following command:

PowerShell
npm install -g @angular/cli

Create ASP.NET Core Web Project from Visual Studio 2017

Make sure you have the latest Visual Studio 2017 (version 15.9.5), and .NetCore 2.2 SDK installed. Download .NET Core 2.2 from here.

Open your Visual Studio 2017 -> Create New Project -> Select Core Web application. Name the solution as Global Weather.

Image 2

Click 'OK' and, in the next window, select an API as shown below:

Image 3

Click 'OK' again to create GlobalWeather solution.

Create Weather Client With Angular CLI

Once the API project is created, open the Powershell and navigate to the GlobalWeather project folder, run the following command:

PowerShell
ng new WeatherClient

Image 4

This will create an Angular 7 application within an API project. Now the solution structure should be like this:

Image 5

Now, we need to make some changes in the default Startup.cs class.

Add the below lines in the ConfigureService method:

C#
services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "WeatherClient/dist";
});

Add the below lines in the Configure method:

C#
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
app.UseSpa(spa =>
{
    spa.Options.SourcePath = "WeatherClient";
    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
});

Remove "launchUrl": "api/values" from Properties/launchSettings.json.

OK. Now just click "IISExpress" to run it.

Image 6

Bang! It’s not working. Basically, the exception is Failed to start npm. But I can tell you, it’s definitely working in NetCore 2.1. So what’s happening for NetCore 2.2? After doing some research, the bad news is it’s a bug of Netcore 2.2, the good news is there is a workaround.

Now we make a workaround to fix it. First, create a class, CurrentDirectoryHelper.cs.

C#
using System;

namespace GlobalWeather
{
    internal class CurrentDirectoryHelpers
    {
        internal const string AspNetCoreModuleDll = "aspnetcorev2_inprocess.dll";

        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        [System.Runtime.InteropServices.DllImport(AspNetCoreModuleDll)]
        private static extern int http_get_application_properties
                      (ref IISConfigurationData iiConfigData);

        [System.Runtime.InteropServices.StructLayout
                   (System.Runtime.InteropServices.LayoutKind.Sequential)]
        private struct IISConfigurationData
        {
            public IntPtr pNativeApplication;
            [System.Runtime.InteropServices.MarshalAs
                    (System.Runtime.InteropServices.UnmanagedType.BStr)]
            public string pwzFullApplicationPath;
            [System.Runtime.InteropServices.MarshalAs
                    (System.Runtime.InteropServices.UnmanagedType.BStr)]
            public string pwzVirtualApplicationPath;
            public bool fWindowsAuthEnabled;
            public bool fBasicAuthEnabled;
            public bool fAnonymousAuthEnable;
        }

        public static void SetCurrentDirectory()
        {
            try
            {
                // Check if physical path was provided by ANCM
                var sitePhysicalPath = Environment.GetEnvironmentVariable
                                       ("ASPNETCORE_IIS_PHYSICAL_PATH");
                if (string.IsNullOrEmpty(sitePhysicalPath))
                {
                    // Skip if not running ANCM InProcess
                    if (GetModuleHandle(AspNetCoreModuleDll) == IntPtr.Zero)
                    {
                        return;
                    }

                    IISConfigurationData configurationData = default(IISConfigurationData);
                    if (http_get_application_properties(ref configurationData) != 0)
                    {
                        return;
                    }

                    sitePhysicalPath = configurationData.pwzFullApplicationPath;
                }

                Environment.CurrentDirectory = sitePhysicalPath;
            }
            catch
            {
                // ignore
            }
        }
    }
}

Then add the below line in Startup method in Startup.cs class:

C#
CurrentDirectoryHelpers.SetCurrentDirectory();

Now run again.

Image 7

That’s it. We make Angular CLI app work with .NetCore perfectly.

Now the framework is done. We need think about what the app need do.

Weather Information REST API

We’re developing a website to display weather information. The user can select whatever location and show the current weather information.

I have decided to use accuweather REST API to acquire data for the application. We need to create an account to obtain an API key to use against the APIs. Users should be able to narrow their location search by country.

Weather Component

Remove everything from app.component.ts, except <router-outlet></router-outlet>.

In Powershell, go to WeatherClient folder. Run the below command to generate new component.

ng generate component weather

Angular Route

Routes tell the router which view to display when a user clicks a link or pastes a URL into the browser address bar.

A typical Angular Route has two properties:

  • path: a string that matches the URL in the browser address bar
  • component: the component that the router should create when navigating to this route

We intend to navigate to the WeatherComponent from the root URL.

Import the WeatherComponent so you can reference it in a Route. Then define an array of routes with a single route to that component.

TypeScript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WeatherComponent } from './weather/weather.component';

const routes: Routes = [
  { path: '', redirectTo: 'weather', pathMatch: 'full' },
  { path: 'weather', component: WeatherComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { } 

Ok. Now refresh the browser, it navigates to WeatherComponent.

Image 8

Reactive Form

Reactive forms provide a model-driven approach to handling form inputs whose values change over time. Reactive forms differ from template-driven forms in distinct ways. Reactive forms provide more predictability with synchronous access to the data model, immutability with observable operators, and change tracking through observable streams. If you prefer direct access to modify data in your template, template-driven forms are less explicit because they rely on directives embedded in the template, along with mutable data to track changes asynchronously.

Let’s build reactive form for Weather Component.

Register ReactiveFormsModule in app.module.ts first.

TypeScript
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { WeatherComponent } from './weather/weather.component';

@NgModule({
  declarations: [
    AppComponent,
    WeatherComponent
  ],
  imports: [
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }   

Build reactive form in ngOnInit() of weather.component.ts.

TypeScript
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-weather',
  templateUrl: './weather.component.html',
  styleUrls: ['./weather.component.css']
})
export class WeatherComponent implements OnInit {

  private weatherForm: FormGroup;

  constructor(
    private fb: FormBuilder) {
  }

  ngOnInit() {
    this.weatherForm = this.buildForm();
  }

  buildForm(): FormGroup {
    return this.fb.group({
      searchGroup: this.fb.group({
        country: [
          null
        ],
        city: [
          null,
          [Validators.required]
        ],
      })
    });
  }
}   

Forms are a fundamental part of every angular application. One of the greatest features of forms is, that you can validate the input of the user before it is sent to the server. Here we validate “city” with the built-in angular validator. Just add required validator to the array of validators. Because we add required validator to “city”, the null value of “city” input makes the form invalid status.

Use [formGroup] and [formControl] in HTML template, weather.component.html.

HTML
<div class="container content" style="padding-left: 0px; padding-top: 10px">
    <form [formGroup]="weatherForm">
        <div formgroupname="searchGroup">
            <div class="row">
                <div class="col-md-3 form-group"><input class="form-control" 
                 formcontrolname="country" id="country" 
                 placeholder="Country" type="text" />
                </div>
            </div>
                
            <div class="row">
                <div class="col-md-3 form-group"><input class="form-control" 
                 formcontrolname="city" id="city" 
                 placeholder="Location" type="text" />
                </div>
            </div>
            
            <div class="row">
                <div class="col-md-3"><input class="btn btn-primary" 
                type="button" /></div>
            </div>
        </div>
    </form>
</div>

Run it again.

Image 9

Use Bootstrap 4 Style in Angular App

Install bootstrap 4 first. In powershell, go to WeatherClient folder, and run the below command:

PowerShell
npm install bootstrap --save

Image 10

In src/styles.css, add the below line:

HTML
@import '../node_modules/bootstrap/dist/css/bootstrap.css';

Now run again.

Image 11

Angular Service

Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data. They should focus on presenting data and delegate data access to a service.

Let’s add location service to call accuweather REST API to get country list.

Create a “shared” folder under “app” folder. Then create “services” and “models” folder under “shared” folder.

From https://developer.accuweather.com/apis, you can get all API references. Now, all we need do is get all countries. The API URL is http://dataservice.accuweather.com/locations/v1/countries.

Create a file called country.ts under src/app/shared/models/ folder. Define a country interface and export it. The file should look like this:

tscript
export interface Country {
  ID: string;
  LocalizedName: string;
  EnglishName: string;
}

Create a file called app.constants.ts in the src/app/ folder. Define locationAPIUrl and apiKey constants. The file should look like this:

tscript
export class Constants {
  static locationAPIUrl = 'http://dataservice.accuweather.com/locations/v1';
  static apiKey = 'NmKsVaQH0chGQGIZodHin88XOpwhuoda';
}

We'll create a LocationService that all application classes can use to get countries. Instead of creating that service with new, we'll rely on Angular dependency injection to inject it into the WeatherComponent constructor.

Using the Angular CLI, create a service called location in the src/app/shared/services/ folder.

ng generate service location

The command generates skeleton LocationService class in src/app/location.service.ts. The LocationService class should look like the following:

JavaScript
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LocationService {

  constructor() { }
}

We must make the LocationService available to the dependency injection system before Angular can inject it into the WeatherComponent. Do this by registering a provider. A provider is something that can create or deliver a service; in this case, it instantiates the LocationService class to provide the service.

Look at the @Injectable() statement right before the LocationService class definition, you can see that the providedIn metadata value is 'root'. When you provide the service at the root level, Angular creates a single, shared instance of LocationService and injects into any class that asks for it. Registering the provider in the @Injectable metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.

Open the WeatherComponent class file. Import the LocationService.

tscript
import { LocationService } from '../shared/services/location.service';

And inject the LocationService.

JavaScript
constructor(
    private fb: FormBuilder,
    private locationService: LocationService) {
}

Angular HttpClient

The LocationService gets countries data with HTTP requests. HttpClient is Angular's mechanism for communicating with a remote server over HTTP.

Open the root AppModule, import the HttpClientModule symbol from @angular/common/http.

JavaScript
import { HttpClientModule } from '@angular/common/http';

Add it to the @NgModule.imports array.

JavaScript
imports: [
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ]

Get Countries with HttpClient

JavaScript
getCountries(): Observable<Country[]> {
    const uri = decodeURIComponent(
      `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
    );
    return this.http.get<Country[]>(uri)
      .pipe(
        tap(_ => console.log('fetched countries')),
        catchError(this.errorHandleService.handleError('getCountries', []))
      );
  }

HttpClient.get returns the body of the response as an untyped JSON object by default. Applying the optional type specifier, <Country[]>, gives you a typed result object.

The shape of the JSON data is determined by the server's data API. Accuweather API returns the country data as an array.

The getCountries method will tap into the flow of observable values. It'll do that with the RxJS tap operator, which looks at the observable values, does something with those values, and passes them along. The tap call back doesn't touch the values themselves.

When things go wrong, especially when you're getting data from a remote server, the LocationService.getCountries() method should catch errors and do something appropriate.

To catch errors, you "pipe" the observable result from http.get() through an RxJS catchError() operator. We warp this to error-handle.service.ts class.

JavaScript
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class ErrorHandleService {
  constructor() {}
  handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error); // log to console instead
      // Let the app keep running by returning an empty result.
      return of(result as T);
    }
  }
}

So the LocationService class looks like the below now:

JavaScript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { Country } from '../../shared/models/country';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from '../../shared/services/error-handle.service';

@Injectable({
  providedIn: 'root'
})
export class LocationService {

  constructor(
    private http: HttpClient,
    private errorHandleService: ErrorHandleService) { }

  getCountries(): Observable<Country[]> {
    const uri = decodeURIComponent(
      `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
    );
    return this.http.get<Country[]>(uri)
      .pipe(
        tap(_ => console.log('fetched countries')),
        catchError(this.errorHandleService.handleError('getCountries', []))
      );
  }
}

Add getCountries() in WeatherComponent file to retrieve the countries from the service.

JavaScript
getCountries(): void {
    this.locationService.getCountries()
      .subscribe(countries => this.countries = countries);
}

Call getCountries() inside the ngOnInit lifecycle hook and let Angular call ngOnInit at an appropriate time after constructing a WeatherComponent instance.

JavaScript
ngOnInit() {
    this.weatherForm = this.buildForm();
    this.getCountries();
  }

Promise

A promise is a special type of Object that we can either use, or construct ourselves to handle asynchronous tasks. Before promises, callbacks were what we used for async functionality, like the above subscribe the http service result. Callbacks are fine till the code doesn’t get complex. But what happens when you have many layers of calls and many errors to handle? You encounter Callback Hell! Promises work with asynchronous operations and they either return us a single value (i.e., the promise resolves) or an error message (i.e., the promise rejects).

Now we promise to rewrite WeatherComponent.getCountries().

JavaScript
async getCountries() {
  const promise = new Promise((resolve, reject) => {
    this.locationService.getCountries()
      .toPromise()
      .then(
        res => { // Success
          this.countries = res;
          resolve();
        },
        err => {
          console.error(err);
          this.errorMessage = err;
          reject(err);
        }
      );
  });
  await promise;
}

Because getCountries() is async function now, we need await this function in ngOnInit().

JavaScript
async ngOnInit() {
    this.weatherForm = this.buildForm();
    await this.getCountries();
  }

AutoComplete of Country Input

Ng-bootstrap is Angular widgets built from the ground up using only Bootstrap 4 CSS with APIs designed for the Angular ecosystem. We use one of the widgets “Typeahead” to implement Country AutoComplete.

NgbTypeahead directive provides a simple way of creating powerful typeaheads from any text input. Use the below command install ng-bootstrap.

JavaScript
npm install --save @ng-bootstrap/ng-bootstrap

Once installed, you need to import our main module.

JavaScript
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
  declarations: [
    AppComponent,
    WeatherComponent
  ],
  imports: [
    NgbModule,
    FormsModule,
    ReactiveFormsModule,
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

On Focus Behaviour

It is possible to get the focus events with the current input value to emit results on focus with a great flexibility. On empty input all options will be taken, otherwise options will be filtered against the search term.

Open weather.component.html, change “country” input to use NgbTypeahead.

HTML
<input type="text" id="country" class="form-control" formControlName="country"
                 placeholder="Country"
                 [ngbTypeahead]="searchCountry" [resultFormatter]="countryFormatter"
                 [inputFormatter]="countryFormatter"
                 (focus)="focus$.next($event.target.value)"
                 (click)="click$.next($event.target.value)"
                 #instanceCountry="ngbTypeahead"
                 autocomplete="off" editable="false" [focusFirst]="false" />

Open weather.component.ts, first import NgbTypeahead.

JavaScript
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';

Then add the below code:

JavaScript
countryFormatter = (country: Country) => country.EnglishName;

  searchCountry = (text$: Observable<string>) => {
    const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
    const clicksWithClosedPopup$ = this.click$.pipe
                   (filter(() => !this.instanceCountry.isPopupOpen()));
    const inputFocus$ = this.focus$;

    return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(
      map(term => (term === ''
        ? this.countries
        : this.countries.filter(v => v.EnglishName.toLowerCase().indexOf
                               (term.toLowerCase()) > -1)).slice(0, 10))
    );
  }

Now run GlobalWeather project via IISExpress. You can see the exact behaviour as expected. Load all countries when input is empty.

Image 12

The options value is filtered by the non-empty value.

Image 13

Search Location

Before we call API to get current conditions of weather, we need to pass location key. So we need to call City Search API first, http://dataservice.accuweather.com/locations/v1/cities/{countryCode}/{adminCode}/search.

Create a file called city.ts under src/app/shared/models/ folder. Define a city interface and export it. The file should look like this:

JavaScript
import { Country } from './country';

export interface City {
  Key: string;
  EnglishName: string;
  Type: string;
  Country:Country;
}

Open location.service.ts under src/app/shared/services/ folder, add getCities method.

JavaScript
getCities(searchText: string, countryCode: string): Observable<City[]> {
  const uri = countryCode
    ? decodeURIComponent(
      `${Constants.locationAPIUrl}/cities/${countryCode}/search?
                      apikey=${Constants.apiKey}&q=${searchText}`)
    : decodeURIComponent(
      `${Constants.locationAPIUrl}/cities/search?apikey=${Constants.apiKey}&q=${searchText}`);
  return this.http.get<City[]>(uri)
    .pipe(
      map(res => (res as City[]).map(o => {
        return {
          Key: o.Key,
          EnglishName: o.EnglishName,
          Type: o.Type,
          Country: {
            ID: o.Country.ID,
            EnglishName: o.Country.EnglishName
          }
        }
      })),
      tap(_ => console.log('fetched cities')),
      catchError(this.errorHandleService.handleError('getCities', []))
    );
}

How to Map Http Json Response to an Object Array

HttpClient is an evolution of the Angular HTTP API, JSON is an assumed default and no longer needs to be explicitly parsed. Map JSON result to an array, especially a complex array is always a little bit tricky. Let’s have a look at how map search location API results to City Array.

From API reference, we define the city interface, which only has the fields we need. For each item in json result, we create a new object and initialize fields from JSON.

JavaScript
map(res => (res as City[]).map(o => {
          return {
            Key: o.Key,
            EnglishName: o.EnglishName,
            Type: o.Type,
            Country: {
              ID: o.Country.ID,
              EnglishName: o.Country.EnglishName
            }
          }
        }))

Get Current Conditions of Weather

http://dataservice.accuweather.com/currentconditions/v1/{locationKey} is the API we need to call to get current conditions.

Create a file called current-conditions.ts under src/app/shared/models/ folder. Define a CurrentConditions interface and export it. The file should look like this:

JavaScript
export interface CurrentConditions {
  LocalObservationDateTime: string;
  WeatherText: string;
  WeatherIcon: number;
  IsDayTime: boolean;
  Temperature: Temperature;
}

export interface Metric {
  Unit: string;
  UnitType: number;
  Value:number;
}

export interface Imperial {
  Unit: string;
  UnitType: number;
  Value: number;
}

export interface Temperature {
  Imperial: Imperial;
  Metric: Metric;
}

Open app.constants.ts under src/app/app.constants.ts. Add a new constant.

JavaScript
static currentConditionsAPIUrl = 'http://dataservice.accuweather.com/currentconditions/v1';

Create a service called current-conditions in the src/app/shared/services/ folder.

JavaScript
ng generate service currentConditions

The command generates skeleton CurrentConditionsService class in src/app/current-conditions.service.ts.

Then add getCurrentConditions method in CurrentConditionsService class.

JavaScript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Constants } from '../../../app/app.constants';
import { CurrentConditions } from '../models/current-conditions';
import { catchError, map, tap } from 'rxjs/operators';
import { ErrorHandleService } from '../../shared/services/error-handle.service';

@Injectable()
export class CurrentConditionsService {

  constructor(
    private http: HttpClient,
    private errorHandleService: ErrorHandleService) { }

  getCurrentConditions(locationKey: string): Observable<CurrentConditions []> {
    const uri = decodeURIComponent(
      `${Constants.currentConditionsAPIUrl}/${locationKey}?apikey=${Constants.apiKey}`
    );
    return this.http.get<CurrentConditions []>(uri)
      .pipe(
        tap(_ => console.log('fetched current conditions')),
        catchError(this.errorHandleService.handleError('getCurrentConditions', []))
      );
  }
}

Get Location Key in WeatherComponent

Open weatherComponent.ts under src/app/weather folder. Add getCity() method.

JavaScript
async getCity() {
  const country = this.countryControl.value as Country;
  const searchText = this.cityControl.value as string;
  const countryCode = country ? country.ID : null;
  const promise = new Promise((resolve, reject) => {
    this.locationService.getCities(searchText, countryCode)
      .toPromise()
      .then(
        res => { // Success
          var data = res as City[];
          const cities = data;
          if (cities.length === 0) {
            this.errorMessage = 'Cannot find the specified location.';
            reject(this.errorMessage);
          } else {
            this.city = cities[0];
            resolve();
          }
        },
        err => {
          console.error(err);
          this.errorMessage = err;
          reject(err);
        }
      );
  });
  await promise;
  if (this.city) {
    const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
    this.weatherForm.patchValue({
      searchGroup: {
        country: country,
        city: this.city.EnglishName
      }
    });
  }
}

Patch the Value of a Form Control

With reactive forms, setting models value are extremely easy to do with the form APIs. There are actually two things happening when updating a FormGroup versus FormControl.

It’s easy to get form control from component. For example, we can get the City and Country form controls as the below:

JavaScript
get cityControl(): FormControl {
    return <FormControl>this.weatherForm.get('searchGroup.city');
  }

get countryControl(): FormControl {
    return <FormControl>this.weatherForm.get('searchGroup.country');
 }

PatchValue’ll allow you to set values that exist and it will ignore ones that do not exist in the current iterated control.

In getCity() function, we patch weather form values when we get the response back.

JavaScript
if (this.city) {
  const country = this.countries.filter(x => x.ID === this.city.Country.ID)[0];
  this.weatherForm.patchValue({
    searchGroup: {
      country: country,
      city: this.city.EnglishName
    }
  });
}

Get Current Conditions in WeatherComponent

Create a file called weather.ts under src/app/shared/models/ folder. Define a Weather class and export it. The file should look like this:

JavaScript
import { CurrentConditions } from './current-conditions';
import { City } from './city';

export class Weather {
  public location: string;
  public weatherIconUrl: string;
  public weatherText: string;
  public temperatureValue: number;
  public temperatureUnit: string;
  public isDaytime: boolean;

  public constructor(currentConditions: CurrentConditions, city: City) {
    this.location = city.EnglishName;
    this.weatherText = currentConditions.WeatherText;
    this.isDaytime = currentConditions.IsDayTime;
    if (currentConditions.WeatherIcon)
      this.weatherIconUrl = `../assets/images/${currentConditions.WeatherIcon}.png`;
    this.temperatureValue = currentConditions.Temperature.Metric.Value;
    this.temperatureUnit = currentConditions.Temperature.Metric.Unit;
  }
}

Open weather.component.ts under, add getCurrentConditions() method. When we get the result from CurrentConditionService, map it to weather class.

JavaScript
async getCurrentConditions() {
    if (!this.city)
      return;
    const promise = new Promise((resolve, reject) => {
      this.currentConditionService.getCurrentConditions(this.city.Key)
        .toPromise()
        .then(
          res => { // Success
            if (res.length > 0) {
              const data = res[0] as CurrentConditions;
              this.weather = new Weather(data, this.city);
              resolve();
            } else {
              this.errorMessage = "Weather is not available.";
              reject(this.errorMessage);
            }
          },
          err => {
            console.error(err);
            reject(err);
          }
        );
    });
    await promise;
  }

Binding HTML Element Disabled with the Valid of Form Group

HTML
input type="button" class="btn btn-primary" 
[disabled]="!weatherForm.valid" value="Go" (click)="search()" />

Go” button only gets enabled when Weather form group is valid. When building the form, the city field is required.

JavaScript
buildForm(): FormGroup {
    return this.fb.group({
      searchGroup: this.fb.group({
        country: [
          null
        ],
        city: [
          null,
          [Validators.required]
        ],
      })
    });
  }

That means if City field is empty, the Weather form group is invalid. And the “Go” button is only enabled if City field has value. And “Click” this button will trigger Search function.

Show Weather Panel in Weather HTML Template

After Search(), we get current conditions and store in weather member of the WeatherComponent class.

Now we need display the search result in Weather template.

Open weather.component.html under src/app/weather folder, add the below change, before <form>. This is a simple Angular template binding. Here, I use ng-template directive to display "Daytime" or "Night".

Like the name indicates, the ng-template directive represents an Angular template: this means that the content of this tag will contain part of a template, that can be then be composed together with other templates in order to form the final component template.

Angular is already using ng-template under the hood in many of the structural directives that we use all the time: ngIf, ngFor and ngSwitch.

HTML
<div class="city">
   <div *ngIf="weather">
     <h1>{{weather.location | uppercase }}</h1>
     <div class="row">
       <table>
         <tr>
           <td>
             <img src="{{weather.weatherIconUrl}}" class="img-thumbnail">
           </td>
           <td>
             <span>{{weather.weatherText}}</span>
           </td>
         </tr>
         <tr>
           <td>
             <div *ngIf="weather.isDaytime; then thenBlock else elseBlock"></div>
             <ng-template #thenBlock><span>Daytime</span></ng-template>
             <ng-template #elseBlock><span>Night</span></ng-template>
           </td>
           <td>
             <span>{{weather.temperatureValue}}&deg;{{weather.temperatureUnit}}</span>
           </td>
         </tr>
       </table>
     </div>
   </div>
   <div *ngIf="!weather">
     <div class="content-spacer-invisible"></div>
     <div> {{errorMessage}}</div>
   </div>
 </div>

Now run the app again.

Image 14

Woo! We get current conditions of Melbourne.

Still a little bit stuff missing.

Component Style

Add component style in weather.component.css under src/app/weather folder.

CSS
.city {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 400px;
  padding: 0px 20px 20px 20px;
  margin: 0px 0px 50px 0px;
  border: 1px solid;
  border-radius: 5px;
  box-shadow: 2px 2px #888888;
}

.city h1 {
  line-height: 1.2
}

.city span {
  padding-left: 20px
}

.city .row {
  padding-top: 20px
}

Weather Icons

Create an “images” folder under src/assets. Download all weather icons from http://developer.accuweather.com, and add them to “images” folder.

Run application again.

Image 15

Debug Angular App from Chrome

Every developer knows debugging is very important for development. Let’s have a look at how to debug Angular app from Chrome.

Run “GlobalWeather” project with IIS Express. Press “F12” to show Developer Tools. Then Click “Source” tab.

Image 16

Find the source typescript file from the webpack:// in left panel. Here, we take weather.componet.ts as an example.

After you select the source file, source code will show in the middle panel. Click the line number will toggle the break point. Put the breakpoint where you want to debug.

Image 17

From UI, Select “Australia” and input “Melbourne”, then click “Go” button, the breakpoint will be hit.

Image 18

How to Use the Source Code

npm install

The source code doesn't include any external package. So before you run it from Visual Studio, you need to run install all dependencies. Open powershell, and go to GlobalWeather\GlobalWeather\WeatherClient folder. Run npm install.

JavaScript
npm install

Then build and run from Visual Stuido.

Accuweather API Key

I removed my API key from the source code. So if you want the source code project working, please register http://developer.accuweather.com to get a free key for yourself.

Conclusion

In this article, we built an Angular 7 application with .NET Core 2.2, and introduced Angular fundamentals, like Bootstrapping, NgModules, Reactive Form, Http Client, Observation, Promise and Routing.

In the next article, Global Weather Part 2, we’ll start to build backend with .NET Core API. We'll use .NetCore API to save the location user selected and populated automatically for the subsequent visits.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Australia Australia
Fred is a senior software developer who lives in Melbourne, Australia. In 1993, he started Programming using Visual C++, Visual Basic, Java, and Oracle Developer Tools. From 2003, He started with .Net using C#, and then expertise .Net development.

Fred is often working with software projects in different business domains based on different Microsoft Technologies like SQL-Server, C#, VC++, ASP.NET, ASP.Net MVC, WCF,WPF, Silverlight, .Net Core and Angular, although he also did some development works on IBM AS400.

Comments and Discussions

 
QuestionHow does that Weather Client work? Pin
Member 1507871628-Jul-22 20:35
Member 1507871628-Jul-22 20:35 
Questionmerge Pin
Gluups28-May-20 22:56
Gluups28-May-20 22:56 
QuestionAny update needed ? Pin
Gluups13-May-20 3:34
Gluups13-May-20 3:34 
AnswerNot now Pin
Gluups15-May-20 2:41
Gluups15-May-20 2:41 
GeneralRe: Not now Pin
Gluups30-May-20 5:22
Gluups30-May-20 5:22 
Questionprivate weatherForms Pin
Gluups28-Apr-20 4:05
Gluups28-Apr-20 4:05 
Question.Net Core 2.2 no longer supported! Pin
Aidan Hutchinson6-Feb-20 2:57
Aidan Hutchinson6-Feb-20 2:57 
QuestionType Error Pin
BrianC7513-May-19 8:46
BrianC7513-May-19 8:46 
AnswerRe: Type Error Pin
BrianC7514-May-19 7:28
BrianC7514-May-19 7:28 
QuestionUsing Visual Studio 2019 Pin
Member 1324199715-Apr-19 5:11
Member 1324199715-Apr-19 5:11 
AnswerRe: Using Visual Studio 2019 Pin
Gluups30-May-20 5:40
Gluups30-May-20 5:40 
SuggestionLittle typo! Pin
Golden00810-Apr-19 9:38
Golden00810-Apr-19 9:38 
GeneralMy vote of 5 Pin
Member 1414833426-Mar-19 17:59
Member 1414833426-Mar-19 17:59 
QuestionI do not understand the handleError function. Pin
Member 1270919821-Mar-19 5:49
Member 1270919821-Mar-19 5:49 
AnswerRe: I do not understand the handleError function. Pin
Fred Song (Melbourne)23-Mar-19 0:46
mvaFred Song (Melbourne)23-Mar-19 0:46 
GeneralRe: I do not understand the handleError function. Pin
Member 1270919824-Mar-19 23:25
Member 1270919824-Mar-19 23:25 
QuestionRegarding the bug Pin
YouSui13-Mar-19 14:39
YouSui13-Mar-19 14:39 
AnswerRe: Regarding the bug Pin
Fred Song (Melbourne)23-Mar-19 0:41
mvaFred Song (Melbourne)23-Mar-19 0:41 
AnswerRe: Regarding the bug Pin
Member 1431771119-Apr-19 10:25
Member 1431771119-Apr-19 10:25 
QuestionIs this a neglect or intentionally done? Pin
YouSui13-Mar-19 14:28
YouSui13-Mar-19 14:28 
GeneralMy vote of 5 Pin
Member 121366198-Mar-19 16:25
Member 121366198-Mar-19 16:25 
SuggestionAngular Routes section - modify text Pin
ciroBorrelli1-Mar-19 0:00
ciroBorrelli1-Mar-19 0:00 
GeneralRe: Angular Routes section - modify text Pin
robertosalemi25-Jun-19 22:28
professionalrobertosalemi25-Jun-19 22:28 
QuestionMinor errors Pin
ChrisVL27-Feb-19 2:22
ChrisVL27-Feb-19 2:22 
AnswerRe: Minor errors Pin
ChrisVL28-Feb-19 0:03
ChrisVL28-Feb-19 0:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.