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.
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:
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
{
var sitePhysicalPath = Environment.GetEnvironmentVariable
("ASPNETCORE_IIS_PHYSICAL_PATH");
if (string.IsNullOrEmpty(sitePhysicalPath))
{
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
{
}
}
}
}
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);
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 => {
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 => {
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 => {
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}}°{{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.