Click here to Skip to main content
13,863,239 members
Click here to Skip to main content
Add your own
alternative version

Stats

27.2K views
764 downloads
71 bookmarked
Posted 17 Jan 2019
Licenced CPOL

Angular 7 with .NET Core 2.2 - Global Weather (Part 1)

, 12 Feb 2019
Rate this:
Please Sign up or sign in to vote.
Angular 7 with .NET Core 2.2 - Global Weather (Part 1)

Introduction

In this article, you 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.

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:

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.

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

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:

ng new WeatherClient

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

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

Add the below lines in the ConfigureService method:

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

Add the below lines in the Configure method:

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.

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.

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:

CurrentDirectoryHelpers.SetCurrentDirectory();

Now run again.

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.

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.

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.

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.

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.

<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.

Use Bootstrap 4 Style in Angular App

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

npm install bootstrap --save

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

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

Now run again.

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:

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:

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:

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.

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

And inject the LocationService.

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.

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

Add it to the @NgModule.imports array.

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

Get Countries with HttpClient

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.

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:

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.

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.

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().

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().

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.

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

Once installed, you need to import our main module.

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.

<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.

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

Then add the below code:

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.

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

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:

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.

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.

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:

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.

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

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

ng generate service currentConditions

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

Then add getCurrentConditions method in CurrentConditionsService class.

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.

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:

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.

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:

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.

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

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.

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.

<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.

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.

.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.

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.

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.

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

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.

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)

Share

About the Author

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

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

You may also be interested in...

Comments and Discussions

 
QuestionCannot start npm Pin
Member 127814908-Feb-19 23:10
memberMember 127814908-Feb-19 23:10 
AnswerRe: Cannot start npm Pin
Fred Song (Melbourne)10-Feb-19 16:07
memberFred Song (Melbourne)10-Feb-19 16:07 
GeneralRe: Cannot start npm Pin
Member 1278149011-Feb-19 6:06
memberMember 1278149011-Feb-19 6:06 
GeneralRe: Cannot start npm Pin
Fred Song (Melbourne)11-Feb-19 11:56
memberFred Song (Melbourne)11-Feb-19 11:56 
QuestionCompiling Issues (Solved) Pin
Cameron Duffy6-Feb-19 14:42
memberCameron Duffy6-Feb-19 14:42 
GeneralMy vote of 5 Pin
Cameron Duffy6-Feb-19 14:31
memberCameron Duffy6-Feb-19 14:31 
GeneralMy vote of 5 Pin
devanganaa6-Feb-19 4:37
memberdevanganaa6-Feb-19 4:37 
Questionmy Note in 1 Pin
mag1329-Jan-19 14:28
membermag1329-Jan-19 14:28 
AnswerRe: my Note in 1 Pin
Sling Blade 226-Feb-19 4:02
memberSling Blade 226-Feb-19 4:02 
GeneralRe: my Note in 1 Pin
mag136-Feb-19 6:41
membermag136-Feb-19 6:41 
PraiseReview Pin
DanLevitan27-Jan-19 6:01
memberDanLevitan27-Jan-19 6:01 
GeneralRe: Review Pin
Fred Song (Melbourne)28-Jan-19 21:37
memberFred Song (Melbourne)28-Jan-19 21:37 
Questionabout API key Pin
Fred Song (Melbourne)25-Jan-19 19:28
memberFred Song (Melbourne)25-Jan-19 19:28 
Questioncode is not working.... see modification... Pin
Arkady Geltzer25-Jan-19 6:13
memberArkady Geltzer25-Jan-19 6:13 
AnswerRe: code is not working.... see modification... Pin
Fred Song (Melbourne)25-Jan-19 13:59
memberFred Song (Melbourne)25-Jan-19 13:59 
AnswerRe: code is not working.... see modification... Pin
RussianTexan9-Feb-19 16:37
memberRussianTexan9-Feb-19 16:37 
SuggestionReplace app.component.ts with app.component.html under 'Weather Component' Pin
Praveen Raghuvanshi23-Jan-19 6:13
professionalPraveen Raghuvanshi23-Jan-19 6:13 
QuestionCannot download the source code Pin
Member 1412082321-Jan-19 23:10
memberMember 1412082321-Jan-19 23:10 
AnswerRe: Cannot download the source code Pin
Fred Song (Melbourne)22-Jan-19 1:23
memberFred Song (Melbourne)22-Jan-19 1:23 
GeneralRe: Cannot download the source code Pin
Member 1412082322-Jan-19 18:42
memberMember 1412082322-Jan-19 18:42 
QuestionCurrently the source code includes my API key. Pin
Marc Clifton21-Jan-19 14:38
protectorMarc Clifton21-Jan-19 14:38 
You really should NOT do that.
Latest Article - A Concise Overview of Threads

Learning to code with python is like learning to swim with those little arm floaties. It gives you undeserved confidence and will eventually drown you. - DangerBunny

Artificial intelligence is the only remedy for natural stupidity. - CDP1802

AnswerRe: Currently the source code includes my API key. Pin
Fred Song (Melbourne)6-Feb-19 19:50
memberFred Song (Melbourne)6-Feb-19 19:50 
QuestionI was excited to use this, but couldn't Pin
RushRules21-Jan-19 6:47
memberRushRules21-Jan-19 6:47 
AnswerRe: I was excited to use this, but couldn't Pin
Fred Song (Melbourne)21-Jan-19 20:26
memberFred Song (Melbourne)21-Jan-19 20:26 
AnswerRe: I was excited to use this, but couldn't Pin
Fred Song (Melbourne)22-Jan-19 1:35
memberFred Song (Melbourne)22-Jan-19 1:35 

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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05 | 2.8.190214.1 | Last Updated 12 Feb 2019
Article Copyright 2019 by Fred Song (Melbourne)
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid