Wednesday, 20 November 2019

Using Popover Controller in Ionic

We will learn how to use Popover Controller in Ionic. Popover is more versatile compares to Alert Controller or Modal Controller. We can put different input fields and elements and have customised template with two ways data binding. Pretty much like a Component.

On this post we will learn how to create a popover with an input field, a select dropdown with a function and buttons. The popover takes data from main page and returns data to the main page.

This is how the popover looks like:


First, create the popover Component:
import { Component } from "@angular/core";
import { PopoverController, Events } from '@ionic/angular';

@Component({    
    templateUrl: './calculate-rent.component.html',
    styleUrls: ['../base/property.page.scss'],
})

export class CalculateRentComponent {
     // some variables to be bound to the template 
    shouldBeWeeklyRent: number;

    constructor(private popoverCtrl: PopoverController, private events: Events) {
        
    }
     
    popoverEvent() {
        this.events.publish('fromPopoverEvent', this.shouldBeWeeklyRent);
        this.popoverCtrl.dismiss();
    }

    closePopOver() {
        this.popoverCtrl.dismiss();
    }
}
We use Events to pass data back to the caller component. Note this.events.publish(‘[name]’, [data]) on the codes.

The template:
<ion-grid>
    <ion-row>
        <ion-col class="text-header" style="text-align: center">Calculate Weekly Rent</ion-col>
    </ion-row>
    <ion-row>
        <ion-col size="8">Desired rental yield:</ion-col>
        <ion-col size="3">
            <input type="text" class="textinput" style="border:1px solid #b2b2b2; background-color: #fff; padding: 0 3px 0 3px; text-align: right; width: 100%"
                   number-input [(ngModel)]="desiredYield" (ngModelChange)="calculateWeeklyRent()" />
        </ion-col>
        <ion-col size="1"><span style="padding-top: 6px; padding-left: 0px">%</span></ion-col>
    </ion-row>
    <ion-row>
        <ion-col size="8">From which type:</ion-col>
        <ion-col size="3">
            <select [(ngModel)]="desiredYieldType" (ngModelChange)="calculateWeeklyRent()">
                <option value="gross">Gross</option>
                <option value="net">Net</option>
            </select>
        </ion-col>
        <ion-col></ion-col>
    </ion-row>
    <ion-row>
        <ion-col size="8">New weekly rent:</ion-col>
        <ion-col><b>{{shouldBeWeeklyRent | currency}}</b></ion-col>
    </ion-row>
    <ion-row>
        <ion-col></ion-col>
    </ion-row>
    <ion-row>
        <ion-col size="4">
            <button type="submit" class="button custom-button" (click)="closePopOver()">
                Close
            </button>
        </ion-col>
        <ion-col size="8">
            <button type="submit" class="button custom-button" (click)="popoverEvent()">
                Put in Rental Field
            </button>
        </ion-col>
    </ion-row>
</ion-grid>

We then add variables for data binding and the function that will be called on the component.

Then on the main page:
async showCalculateWeeklyRent(ev) {
 const popover = await this.popoverCtrl.create({
  component: CalculateRentComponent, // the popover component that we created
  event: ev,
  componentProps: { // data to be passed
   totalCost: this.model.totalCost,
   totalExpense: this.model.totalExpense,
   totalIncomeWithoutRent: this.model.totalIncomeWithoutRent
  },
  cssClass: 'popoverClass',
 });

 // sync event from popover component
 this.events.subscribe('fromPopoverEvent', (shouldBeWeeklyRent) => {
  this.model.weeklyRent = shouldBeWeeklyRent;
  this.calculateYearlyRent();
 });

 return await popover.present();
}

Note the componentProps property can be used to pass data to popover component. It can contain objects that have more complex structure as well.
We use Events and its subscribe() method to receive data back.

On popover component, to receive data from the main page, we just need to declare local variables then they will be bound automatically with the data passed from the main page. We just need to make sure the variable names are similar.
// codes in main page
async showCalculateWeeklyRent(ev) {
 const popover = await this.popoverCtrl.create({
  . . .
  componentProps: { // data to be passed
   totalCost: this.model.totalCost,
   totalExpense: this.model.totalExpense,
   totalIncomeWithoutRent: this.model.totalIncomeWithoutRent
  },
  . . .
 });

 . . .
}

// codes in popover
export class CalculateRentComponent {
    . . .
    totalCost: number;
    totalExpense: number;
    totalIncomeWithoutRent: number;

    // note that even we don’t need to state the variables in the constructor
    constructor(private popoverCtrl: PopoverController, private events: Events) {
        . . .
    }
    . . .
}

Now the full codes in popover component:
import { Component } from "@angular/core";
import { PopoverController, Events } from '@ionic/angular';

@Component({    
    templateUrl: './calculate-rent.component.html',
    styleUrls: ['../base/property.page.scss'],
})
export class CalculateRentComponent {
    desiredYield: number;
    desiredYieldType: string = 'gross';
    shouldBeWeeklyRent: number;
    totalCost: number;
    totalExpense: number;
    totalIncomeWithoutRent: number;

    constructor(private popoverCtrl: PopoverController, private events: Events) {
    }
    
    calculateWeeklyRent() {
        . . .
        this.shouldBeWeeklyRent = …;
        . . .
    }

    popoverEvent() {
        this.events.publish('fromPopoverEvent', this.shouldBeWeeklyRent);
        this.popoverCtrl.dismiss();
    }

    closePopOver() {
        this.popoverCtrl.dismiss();
    }
}


Reference:
https://edupala.com/ionic-4-popover/

Monday, 28 October 2019

Getting and Listing Latest Data in Angular

We will look on how to get and have up to date records to be displayed on a view in Angular 8. Assume we divide our codes with repository, service and presentation layers.

First, on repository, we have our codes returning a Promise:
getAllItems() : Promise<ItemInfo[]> {
 return new Promise((resolve, reject) => {
  this.dbInstance.executeSql("SELECT * FROM Item", [])
   .then((rs) => {
    . . .
    resolve(itemsList);
   })
   .catch((err) => reject(err));   
 });
};

We also have this on the repository layer that will be used by service layer to know that the database is ready to be used:
private dbReady: BehaviorSubject<boolean> = new BehaviorSubject(false);

constructor(private plt: Platform) {
 this.plt.ready().then(() => {
  this.initialise()
   .then(() => {
    this.dbReady.next(true);
   })
   .catch((err) => console.error(err)); 
 });
}

getDatabaseState() {
 return this.dbReady.asObservable();
}

Then on the service. Notice that we will be using BehaviorSubject type and its next() method to announce to listeners that there's a change.
export class ItemService {

    // this is a handy variable used to keep the latest data in memory
    private _itemsData: ItemInfo[] = [];

    private _items: BehaviorSubject<ItemInfo[]> = new BehaviorSubject<ItemInfo[]>([]);

    // getter that will be used by the presentation layer to get the items
    get items(): Observable<ItemInfo[]>  {
        return this._items.asObservable();
    }

    constructor(private databaseService: DatabaseService) {
        this.databaseService.getDatabaseState().subscribe(ready => {
            if (ready) {
                this.databaseService.getAllItems()
                    .then((items) => {
                        this._itemsData = items;

                        this._items.next(this._itemsData);
                    })
                    .catch(error => {
                        console.error(error);
                    });
            }
        });
 }

    addItemDetails(item: ItemForm) {
        return new Promise((resolve, reject) => {
            this.databaseService.insertItemDetails(item)
                .then((newItemId) => {                   
                    this._itemsData.push(new ItemInfo(. . .));
                    this._items.next(this._itemsData);
                    resolve();
                })
                .catch(error => {
                    console.error(error);
                    reject('Error: item cannot be inserted into database.')
                });
        });
    }

    editItemDetails(item: ItemForm) {
        return new Promise((resolve, reject) => {
            this.databaseService.updateItemDetails(item)
                .then(() => {
                    for (let i of this._itemsData) {
                        if (i.itemId == i.itemId) {
                            . . .
                        }
                    }
                    this._items.next(this._itemsData);
                    resolve();
                })
                .catch(error => { reject("Error: item cannot be updated in database."); });
        });
    }

    // similarly when deleting, the local variable _itemsData will need to be updated then call the BehaviorSubject next() method to tell the listeners that there is a change.
}

On our presentation (component .ts file), we use:
export class ListItemsPage implements OnInit {
    items: Observable<ItemInfo[]>;

    constructor(private service: ItemService) {
    }

    ngOnInit() {
        this.items = this.service.items;
    }
}

Finally, on the template .html page, we have:
<ion-list>
 <ion-item *ngFor="let i of items | async">
  <ion-label>
   <div>{{i.name}}</div>
  </ion-label>
 </ion-item>
</ion-list>

Tuesday, 17 September 2019

Converting AngularJS $q to JavaScript Promise

Since I had to upgrade my app from AngularJS to Angular, I would also need to change my promise returned codes. I needed to change all of my codes that were using $q AngularJS service to the new JavaScript Promise.
These codes:
getItems = function () {
  var deferred = $q.defer();

  try {
   . . .
   deferred.resolve(itemsToBeReturned);
  }
  catch(error) {
   . . .
   deferred.reject(error);
  }
  
  return deferred.promise;
};
are converted into:
getItems() {
 return new Promise((resolve, reject) => {
   try {
    . . .
    resolve(itemsToBeReturned);
   }
   catch(error) {
    . . .
    reject(error);
   }
 });
};
The consuming function can remain the same:
this.getItems()
 .then((items) => {
  // success
  . . .
 })
 .catch(error => {
  // error
  . . .
 });

The new JavaScript Promise also allowed the promises returned to be chained:
getItems() {
 return new Promise((resolve, reject) => {
  this.doSomething()
   .then((rs) => {
    . . .
    return Promise.resolve(123); 
   })
   .then((rs) => {
    . . .
    return 456;   // this returns a promise as well
   })
   .then((rs) => { 
    . . .
    resolve(result); 
   })
   .catch(error => reject(error));
 });
};


Reference:
Promise - TypeScript Deep Dive

Wednesday, 4 September 2019

Easy Way to Highlight Invalid Fields in Submitted Angular Form

Here is one way to highlight invalid input fields after submitting a form in Angular (Template Driven Form approach) to help user quickly seeing the invalid input fields.

First we create a variable to indicate whether the form has been submitted or not:
export class PropertyPage {
    isSubmitted: boolean;

    constructor() {
        this.isSubmitted = false;
        . . .
    }
 
    addPropertyDetails(form: NgForm) {
        this.isSubmitted = true;
        . . .
        if (form.valid) {
            . . .
        }  
    }
}

Then use this variable to assign a CSS class to the form using ngClass:
<form #propertyDetailsForm="ngForm" (ngSubmit)="addPropertyDetails(propertyDetailsForm)" [ngClass]="{'submitted': isSubmitted}">
    . . .
</form>

Finally, add the style in the page stylesheet. By default, invalid input field will be assigned ng-invalid class by Angular.
.submitted input.ng-invalid {
    border: 1px solid #f00;
}

Thursday, 8 August 2019

Commands to Check Ionic, Cordova and Plugin Version

Cordova
see installed version
cordova -v
or
cordova --version
see the latest version available
npm info cordova
to upgrade to latest version
npm update -g cordova
-g means install globally

otherwise need to uninstall and reinstall
npm uninstall -g cordova
npm install -g cordova

Ionic CLI

installed version
ionic -v
or
ionic --version
latest version available
npm info ionic
to upgrade to latest version
npm update -g ionic
-g means install globally

otherwise need to uninstall and reinstall
npm uninstall -g ionic
npm install -g ionic

Plugin of an Cordova app
installed version
npm list thePluginName
latest version available
npm info thePluginName
to upgrade to latest version
npm update thePluginName

otherwise need to uninstall and reinstall

or you can try to use cordova-check-plugins plugin
to add to project
cordova plugin add thePluginName
or
ionic cordova plugin add thePluginName
to remove from project
cordova plugin remove thePluginName
or
ionic cordova plugin remove thePluginName

You can also use
ionic info
command in an App directory to check Ionic CLI, Ionic Framework, Cordova CLI, Platforms, Plugins installed and other information.

Monday, 29 July 2019

ASP.NET Core Client Server JWT Authentication

Recently, I was trying to play around with ASP.NET Core JWT Authentication with Web API as backend server and Angular as front end client. I used ASP.NET Core 2.1 version.

Web API server setup
1. Add these codes inside ConfigureService method on Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
   options.TokenValidationParameters = new TokenValidationParameters
   {
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,

    ValidIssuer = "ABC",
    ValidAudience = "DEF",
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("mySuperSecretKey"))
   };
  });

 services.AddCors(options =>
 {
  options.AddPolicy("EnableCORS", builder =>
  {
   builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials().Build();
  });
 });
}
Notice that we also need to enable Cross Origin Resource Sharing (CORS) as our Angular client will sit on different domain.

2. Then inside Configure method, add:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
 if (env.IsDevelopment())
 {
  app.UseDeveloperExceptionPage();
 }
 else
 {
  app.UseHsts();
 }

 app.UseAuthentication();

 app.UseCors("EnableCORS");

 app.UseHttpsRedirection();
 app.UseMvc();
}
Make sure it is before app.UseMvc(); line otherwise you will keep getting ‘401 Unauthorized’ message with no details. Also we need to add CORS setting that we have done.

3. On the Web API login method on controller:
[EnableCors("EnableCORS")]
[HttpPost, Route("login")]
public IActionResult Login([FromBody]LoginModel user)
{
 if (user == null)
 {
  return BadRequest("Invalid client request");
 }

 if (user.UserName == "user" && user.Password == "password")
 {
  var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("mySuperSecretKey "));
  var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

  var tokeOptions = new JwtSecurityToken(
   issuer: "ABC",
   audience: "DEF",
   claims: new List<Claim>(),
   expires: DateTime.Now.AddMinutes(5),
   signingCredentials: signinCredentials
  );

  var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
  return Ok(new { Token = tokenString });
 }
 else
 {
  return Unauthorized();
 }
}
Make sure [EnableCors("EnableCORS")] is added either to the method or at controller level.

4. On our Angular client we will need to call login:
login(form: NgForm) {
    let credentials = JSON.stringify(form.value);
    this.http.post("http://localhost:5000/api/auth/login", credentials, {
      headers: new HttpHeaders({
        "Content-Type": "application/json"
      })
    }).subscribe(response => {
      let token = (<any>response).token;
      localStorage.setItem("jwt", token);
      this.invalidLogin = false;
      this.router.navigate(["/"]);
    }, err => {
      this.invalidLogin = true;
    });
}
Once authenticated, we use local storage to store the token with ‘jwt’ key.

5. Then on our secured resource controller:
[EnableCors("EnableCORS")]
[HttpGet, Authorize]
public IEnumerable<string> Get()
{
 return new string[] { "confidential one", " confidential two" };
}

6. To request that API, we simply use this on client side:
let token = localStorage.getItem("jwt");
this.http.get("http://localhost:5000/api/customer", {
  headers: new HttpHeaders({
 "Authorization": "Bearer " + token
  })
}).subscribe(response => console.log(response)); 

Note that we need to have "Authorization": "Bearer " + token in the request header so that the server can authorize it.  

Friday, 21 June 2019

Should I Change My App's WebSQL Database to Other?

Recently I have been doing app upgrade due to requirements by Google Play and Apple App Store. I have been doing some research about the latest technologies for the app. I tried to find out more about latest database technology and was trying to figure out whether I should change my WebSQL database.

These are the findings that I had in bullet points:
  • It is mentioned that WebSQL has been deprecated but is still supported by Android and iOS. They do not have any plan to remove it anytime in the future.
  • The SQLite alternative is seemed to be supported by individuals/small group as well as those who developed WebSQL. There is no clarity about the support and future as well.
  • IndexedDB seems good but is not supported by iOS
  • Some opinions said that WebSQL is deprecated simply because it does not fulfill a standard for client side storage but the standard does not really exist and not accepted by all parties.
  • SQLite also does not fullfil the standard
Since there is no alternative and clarity about this and major operating systems have no plan to remove WebSQL in the future, I think it is better to keep using WebSQL in my upgraded app.


References:
https://softwareengineering.stackexchange.com/questions/220254/why-is-web-sql-database-deprecated
https://www.reddit.com/r/SQL/comments/8woehg/sqlite_being_deprecatedreplaced_as_database/
https://cordova.apache.org/docs/en/latest/cordova/storage/storage.html

Friday, 26 April 2019

Angular Directive Example to Format Value to Currency

The following is an example of an Angular 6 directive that format input value to currency when losing focus. It uses NgModel for value binding and CurrencyPipe for the formatter.
import { Directive } from '@angular/core';
import { NgModel } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';

@Directive({
  selector: '[ngModel][my-directive]',
  providers: [NgModel, CurrencyPipe],
  host: {
    '(blur)': 'onInputChange($event)'
  } 
})
export class MyDirective {

  constructor(private model: NgModel, private currencyPipe: CurrencyPipe) { }

  onInputChange($event) {
    var value = $event.target.value;
    if (!value) return;

    var plainNumber: number;
    var formattedValue: string;


    var decimalSeparatorIndex = value.lastIndexOf('.'); 
    if (decimalSeparatorIndex > 0) {
      // if input has decimal part
      var wholeNumberPart = value.substring(0, decimalSeparatorIndex);
      var decimalPart = value.substr(decimalSeparatorIndex + 1);
      plainNumber = parseFloat(wholeNumberPart.replace(/[^\d]/g, '') + '.' + decimalPart)
    } else {
      // input does not have decimal part
      plainNumber = parseFloat(value.replace(/[^\d]/g, ''));
    }

    if (!plainNumber) {
      formattedValue = '';
    }
    else {
      formattedValue = this.currencyPipe.transform(plainNumber.toFixed(2), "USD", "symbol-narrow");
    }

    this.model.valueAccessor.writeValue(formattedValue);
  }
}

Then to use it on HTML part:
<input name="productPrice" [(ngModel)]="price" my-directive />

Friday, 5 April 2019

Angular Directive Example to Allow Certain Values

Below is an example of an Angular 6 Directive. This directive detects changes in NgModel as user enters in input value and only allows specific value to be entered while rejecting the others.
import { Directive } from '@angular/core';
import { NgModel } from '@angular/forms';

@Directive({
  selector: '[ngModel][my-directive]',
  providers: [NgModel],
  host: {
    '(ngModelChange)': 'onInputChange($event)'
  } 
})
export class MyDirective {

  constructor(private model: NgModel) { }

  onInputChange(value) {
    console.log(value);
    
    this.model.valueAccessor.writeValue(value.replace(/[^\d\.\,\s]+/g, ''));
  }
}

Then to use it on HTML part:
<input name="productPrice" [(ngModel)]="price" my-directive />

Friday, 22 March 2019

Angular Form, ngModel and Detecting Changes

Angular (at the moment of writing is version 8) has two flavours when coming down to forms. We can use Template Driven or Reactive Form. Template Driven form is useful for simple form that does not need much customisation. It is similar to the form in AngularJS. Angular will create form model representation and data binding in the background with some default structure and names. That is why it is called Template Driven.

Reactive Form, on the other hand is much more flexible however requires more effort in setting up. This form uses more code in component and less in HTML. Reactive form is best to handle complex scenarios and allow easier unit testing.

We are going to see an example of Template Driven form with ngModel binding.
<form name="personNameForm" novalidate="" ng-submit="submitForm()">
 <div>
  <input type="text" name="firstName" class="textinput"
                 [(ngModel)]="person.firstName"
                 (ngModelChange)="combineNames()"
                 required />
  <input type="text" name="lastName" class="textinput"
                 [(ngModel)]="person.lastName"
                 (ngModelChange)="combineNames()"
                 required />
 </div>
</form>
Note that we need to put form's name attribute and for each ngModel field a name attribute is also required.
We use ngModelChange directive to detect changes for NgModel value. $watch and $observe are not required anymore.

Monday, 14 January 2019

My Notes of Switching from Visual Studio to Ionic CLI

Recently, I tried to update my app in Google Play and Apple Store but failed to do so because they mentioned that I need to use later version of Android or iOS. I have been using Visual Studio 2013 then Visual Studio 2016 with Tools for Apache Cordova however when checking the latest supported Android and iOS versions, they have not progressed much in the last two years. After some research, I decided to use Apache Cordova directly, together with Ionic, Angular and Node.js. Apache Cordova and Ionic are still progressing and very much alive. I still use Visual Studio 2017 though for code editor only. All project configurations, simulator settings and others will be done at lower level, using Apache Cordova directly. As my app was written in JavaScript and HTML using Ionic 1 so I think it is a good choice.

The notes below are my journey to move from Visual Studio Tool for Apache Cordova to the CLI option. I use same machine to install the required new components. Hopefully it can help anyone who chooses a similar path as mine.


SETTING UP FRAMEWORKS
First I had Node.js, recent Cordova and Ionic installed. Once they were installed I tried to create a new test app by running:
ionic start myApp sidemenu
then it will show:
+ ionic@4.2.1
added 242 packages from 151 contributors in 43.756s
? Integrate your new app with Cordova to target native iOS and Android?
Chose yes and continued with the installation.
After installation:
cd myApp
ionic serve
I made sure the app could be compiled successfully and showed in a browser.

Then I added Android platform:
ionic cordova platform add android
To check the installed version run:
cordova platform version android
Then it showed:
7.1.1 is the latest version


SETTING UP EMULATOR
Next, I tried to set up the emulator with latest version of Android. But first I wanted to make sure that I could run an existing virtual device from AVD Manager.

When trying to run one, I got this error:
“Could not find an installed version of Gradle either in Android Studio,
or on your system to install the gradle wrapper. Please include gradle
in your path, or install Android Studio
[ERROR] An error occurred while running subprocess cordova.”
I installed Gradle from the website and set an environment variable for it.

The second error I got after trying to run the emulator:
“Could not unzip C:\Users\rical\.gradle\wrapper\dists\gradle-4.1-all\bzyivzo6n839fup2jbap0tjew\gradle-4.1-all.zip to C:\Users\rical\.gradle\wrapper\dists\gradle-4.1-all\bzyivzo6n839fup2jbap0tjew.
Reason: error in opening zip file
Exception in thread "main" java.util.zip.ZipException: error in opening zip file”
I deleted the zip folder and run the command again.

Then I got another error:
“Error occurred during initialization of VM
Could not reserve enough space for 2097152KB object heap”
I went to Start -> Control Panel -> System -> Advanced(tab) -> Environment Variables -> System Variables and add new variable:
Variable name: _JAVA_OPTIONS
Variable value: -Xmx512M

After trying to run the emulator again, another error shown up:
“A problem occurred configuring project ':CordovaLib'.
> You have not accepted the license agreements of the following SDK components:
[Android SDK Platform 27, Android SDK Build-Tools 26.0.2].
Before building your project, you need to accept the license agreements and complete the installation of the missing components using the Android Studio SDK Manager.”
Went to SDK Manager and install SDK Platform 27 and Android SDK Build-Tools 26.0.2.

The emulator is working now, but I received a warning:
“Running an x86 based Android Virtual Device (AVD) is 10x faster. We strongly recommend creating a new AVD.”
Solved this by installing Intel x86 Atom_64 or Intel x86 Atom and setting the emulator (AVD) to use one of them.
I received another error “PANIC: Cannot find AVD system path. Please define ANDROID_SDK_ROOT”. I created a new environment variable called ANDROID_SDK_ROOT that has my SDK path, something like "C:\Program Files (x86)\Android\android-sdk". Then restarted the machine.

I wanted to use recent version of Android which is version 8 or 9 but after installing the system images, I got this error:
“This AVD's configuration is missing a kernel file! Please ensure the file "kernel-qemu" is in the same location as your system image”
I found a great article to solve the issue https://www.andreszsogon.com/using-android-8-and-9-emulators-without-android-studio
I followed the instructions:
- download emulator-windows-4848055.zip
- uninstall my current Android 8 and newer system images
- close my Android SDK Manager and AVD Manager tools if open
- extract the contents from the ZIP file into my android-sdk/tools
- download the desired emulator’s System Images from the SDK Manager
- create a new emulator from the AVD Manager
- start a virtual device

Received another error:
“emulator: ERROR: x86 emulation currently requires hardware acceleration!
Please ensure Windows Hypervisor Platform (WHPX) is properly installed and usable.
CPU acceleration status: HAXM is not installed on this machine”
Went to Turn Windows features on and off, checked Windows Hypervisor Platform.

Finally, I could run a virtual device from the AVD Manager.

When I tried to run from command prompt:
ionic cordova emulate --target=My_Android_9_Virtual_Device  android
I got another error:
“A problem occurred configuring project ':CordovaLib'.
> Failed to find Platform SDK with path: platforms;android-27”
I found out that cordova-android 7.1.1 only supports up to Android 8.1 (SDK 27). So I needed to download the SDK Platform and a choice of system image of the newer version.

Finally, the emulator from command prompt is working!