Blog / Mobile App
Build A Mobile Application With Ionic and .Net MVC Core
  • Jan 26, 2021
  • 82
  • 89

In the previous post, we have learnt about how to build a website using .Net MVC Core. This time we will create a mobile version for this application using the same .Net MVC Core project we created last time and Ionic Framework.

This app is available on Google Play Store: https://play.google.com/store/apps/details?id=lucas.lucasology


Advertisement
 


1. The Project Overview - What are we building?

We are going to build a simple mobile application using Ionic Framework. The application allows user to read articles from lucasology.com website as well as seeing the author's information. 

General requirements:

  • The application has side menu and tab menu for navigation
  • The application shows loading message while fetching data
  • Same data source from lucasology.com

 


Module 1: Home



Module 2: About

  • Show author's information and contact


2. What are we going to use?

We have all the requirements for this web application. Now we have to decide the technologies we are going to use in order to build our Mobile application.

Tools:

Frameworks & Libraries:

  • Ionic
  • Angular

Prerequisites:

3. Create Web API Service for Mobile Application

Using the same .Net MVC Core project we created last time to create Web API Service which will be utilized in the Mobile Application to load data.

Step 1: Create API Controllers

Inside Blog.Web/Controllers, create a new folder called API and create 2 Controllers:

  • AboutMeController
  • BlogController

All API Methods will be called by the API URL in below format:

https://lucasology.com/api/Controller/Action/id

AboutMeController.cs:

using AutoMapper;
using Blog.BAL.Interfaces;
using Blog.BAL.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Blog.Web.Controllers.API
{
    [Route("api/Blog")]
    public class BlogController : Controller
    {
        private readonly IBlogManager _blogManager;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IMapper _mapper;

        public BlogController(IBlogManager blogerManager, IMapper mapper, 
            IHttpContextAccessor httpContextAccessor)
        {
            _blogManager = blogerManager;
            _mapper = mapper;
            _httpContextAccessor = httpContextAccessor;
        }

        [Produces("application/json")]
        [Route("GetPublicPosts")]
        public IActionResult GetPublicPosts(int? pageSize, int? pageNumber)
        {
            pageSize = pageSize == null ? 10 : pageSize;
            pageNumber = pageNumber == null ? 0 : pageNumber;

            var posts = _blogManager.GetPosts(null).OrderByDescending(p => p.PublishedDate)
                .Skip((int)pageNumber * (int)pageSize).Take((int)pageSize).ToList();
            var vm = _mapper.Map<List<PostViewModel>>(posts);
            return Json(new { success = true, 
                message = "Get Posts Successfully!", 
                data = vm });
        }


        [Produces("application/json")]
        [Route("GetPostDetails")]
        public IActionResult GetPostDetails(string id)
        {
            Guid postID = new Guid(id);

            var post = _blogManager.GetPostByID(postID);
            var vm = _mapper.Map<PostViewModel>(post);
            vm.Content = _blogManager.GetContentByUrl(vm.Content)
                .Replace("blockquote", "div")
                .Replace("src=\"/Uploads", $"src=\"https://{_httpContextAccessor.HttpContext.Request.Host.Value}/Uploads");
            return Json(new
            {
                success = true,
                message = "Get Post Detail Successfully!",
                data = vm
            });
        }
    }
}

BlogController.cs

using AutoMapper;
using Blog.BAL.Interfaces;
using Blog.BAL.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Blog.Web.Controllers.API
{
    [Route("api/Blog")]
    public class BlogController : Controller
    {
        private readonly IBlogManager _blogManager;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IMapper _mapper;

        public BlogController(IBlogManager blogerManager, IMapper mapper, 
            IHttpContextAccessor httpContextAccessor)
        {
            _blogManager = blogerManager;
            _mapper = mapper;
            _httpContextAccessor = httpContextAccessor;
        }

        [Produces("application/json")]
        [Route("GetPublicPosts")]
        public IActionResult GetPublicPosts(int? pageSize, int? pageNumber)
        {
            pageSize = pageSize == null ? 10 : pageSize;
            pageNumber = pageNumber == null ? 0 : pageNumber;

            var posts = _blogManager.GetPosts(null).OrderByDescending(p => p.PublishedDate)
                .Skip((int)pageNumber * (int)pageSize).Take((int)pageSize).ToList();
            var vm = _mapper.Map<List<PostViewModel>>(posts);
            return Json(new { success = true, 
                message = "Get Posts Successfully!", 
                data = vm });
        }


        [Produces("application/json")]
        [Route("GetPostDetails")]
        public IActionResult GetPostDetails(string id)
        {
            Guid postID = new Guid(id);

            var post = _blogManager.GetPostByID(postID);
            var vm = _mapper.Map<PostViewModel>(post);
            vm.Content = _blogManager.GetContentByUrl(vm.Content)
                .Replace("blockquote", "div")
                .Replace("src=\"/Uploads", $"src=\"https://{_httpContextAccessor.HttpContext.Request.Host.Value}/Uploads");
            return Json(new
            {
                success = true,
                message = "Get Post Detail Successfully!",
                data = vm
            });
        }
    }
}

Step 2: Enable CORS

We can enable CORS in StartUp.cs. CORS is Cross Origin Resource Sharing which allows Ionic to make request to .Net Core server to get data.

StartUp.cs

public void ConfigureServices(IServiceCollection services)
{
    //Omitted Code ...
    services.AddCors();
}


4. Build the Mobile Application using Ionic Framework

We now can build the Mobile Application using Visual Studio Code and Ionic Framework. Make sure you already installed Node.js. Open Visual Studio Code and start making product!

Step 1: Set up the project

In Visual Studio Code, open Terminal from top menu:



Install Ionic by running below command in terminal:

npm install -g ionic

Navigate to the desired folder by running below command in terminal:

cd C:\Users\Your Folder Path...

Create a blank Ionic project by running below command in terminal:

ionic start Blog.Mobile blank

Select Angular when asked. Angular will be installed along with the Ionic project.

Once the project is created, run below command to test the project:

ionic serve -l


Step 2: Create the project structure

In src folder, create below sub-folders:

  • models
  • pages
  • services

Move the home folder inside pages folder and update the home's routing app-routing.module.ts like below:

./home/home.module --> ./pages/home/home.module


Step 3: Create Tabs and Menu Pages:

Run below commands in Terminal to create Tabs and Menu pages:

ionic g page pages/tabs
ionic g page pages/menu

Also, run below commands in Terminal to create other pages:

ionic g page pages/about
ionic g page pages/post-detail

In models folder, create below files:

  • interface.ts

In services folder, create below files:

  • apiService.ts
  • messageService.ts
  • ownerService.ts
  • postService.ts

The app folder will looks like below after you create all files:



Step 4: Code the project

Below are code in each file:

1. models folder

interface.ts:

export interface IResponse {
    message: string,
    success: boolean,
    data?: any
}

2. pages folder

a. about

about-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { AboutPage } from './about.page';

const routes: Routes = [
  {
    path: '',
    component: AboutPage
  }
];

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

about.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { AboutPageRoutingModule } from './about-routing.module';

import { AboutPage } from './about.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    AboutPageRoutingModule
  ],
  declarations: [AboutPage]
})
export class AboutPageModule {}

about.page.html

<ion-header [translucent]="true">
  <ion-toolbar mode="ios">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>About</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  
  <ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="chevron-down-circle-outline"
      pullingText="Pull to refresh"
      refreshingSpinner="circles"
      refreshingText="Refreshing...">
    </ion-refresher-content>
  </ion-refresher>
  
  <ion-card>
    <ion-avatar>
      <img id="profImage" src="{{owner.profileImage}}">
    </ion-avatar>
    <ion-card-content class="ion-text-center">
      <b>{{owner.name}}</b>
    </ion-card-content>
    <ion-card-content>
      {{owner.summary}}
    </ion-card-content>
  </ion-card>

  <ion-card>
    <ion-card-header class="ion-text-center">
      <b>Follow Me</b>
    </ion-card-header>

    <ion-card-content class="p-0">
      <ion-list>
        <ion-item href="https://lucasology.com">
          <ion-avatar slot="start">
            <ion-icon name="globe-outline"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>Lucasology</h2>
            <h3>My Website</h3>
          </ion-label>
        </ion-item>

        <ion-item href="https://www.linkedin.com/in/nguyenhm/">
          <ion-avatar slot="start">
            <ion-icon name="logo-linkedin"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>LinkedIn</h2>
            <h3>nguyenhm</h3>
          </ion-label>
        </ion-item>

        <ion-item href="https://github.com/lucas-ngminh">
          <ion-avatar slot="start">
            <ion-icon name="logo-github"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>Github</h2>
            <h3>lucas-ngminh</h3>
          </ion-label>
        </ion-item>
        
        <ion-item href="https://www.youtube.com/channel/UCMc-1Gb8xFkyUIlwqpPmS1Q">
          <ion-avatar slot="start">
            <ion-icon name="logo-youtube"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>Youtube</h2>
            <h3>Lucasology</h3>
          </ion-label>
        </ion-item>

        <ion-item href="https://www.facebook.com/lucasology">
          <ion-avatar slot="start">
            <ion-icon name="logo-facebook"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>Facebook</h2>
            <h3>lucasology</h3>
          </ion-label>
        </ion-item>

        <ion-item href="https://www.instagram.com/lucas.ology">
          <ion-avatar slot="start">
            <ion-icon name="logo-instagram"></ion-icon>
          </ion-avatar>
          <ion-label>
            <h2>Instagram</h2>
            <h3>lucas.ology</h3>
          </ion-label>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
</ion-content>
about.page.scss
ion-card {
    padding: 5px;
}

ion-avatar {
    margin: 0 auto;
}

ion-icon {
    padding: 25%;
}

.p-0 {
    padding: 0;
}

about.page.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { AboutPage } from './about.page';

describe('AboutPage', () => {
  let component: AboutPage;
  let fixture: ComponentFixture<AboutPage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AboutPage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(AboutPage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

about.page.ts

import { Component, OnInit } from '@angular/core';
import { NavController, LoadingController, ModalController, AlertController, MenuController } from '@ionic/angular';
import { Platform } from '@ionic/angular';
import { IonInfiniteScroll } from '@ionic/angular';
import { Router, NavigationExtras } from '@angular/router'

import { MessageService } from '../../services/messageService';
import { OwnerService } from '../../services/ownerService';

@Component({
  selector: 'app-about',
  templateUrl: './about.page.html',
  styleUrls: ['./about.page.scss'],
})

export class AboutPage {
  public owner: any;

  constructor(public navCtrl: NavController
    , public loadingCtrl: LoadingController
    , public menuCtrl: MenuController
    , public modalCtrl: ModalController
    , public alertCtrl: AlertController
    , public messageService: MessageService
    , private ownerService: OwnerService
    , private plform: Platform
    , private router: Router) {
    this.menuCtrl.enable(true, "mainMenu");
    this.owner = {};
    // this.isItemAvailable = false;
    this.getOwnerInfo();
  }

  doRefresh(event) {
    console.log('Begin async operation');

    setTimeout(() => {
      this.getOwnerInfo();
      console.log('Async operation has ended');
      event.target.complete();
    }, 100);
  }

  async getOwnerInfo() {
      const loader = await this.loadingCtrl.create({
        spinner: 'dots',
        duration: 5000,
        message: 'Please wait...',
        translucent: true,
        cssClass: 'custom-class custom-loading'
      });

      await loader.present().then(() => {
        this.ownerService.getAboutMe().subscribe((response) => {
          if (response.success) {
            this.owner = response.data;
            console.log(JSON.stringify(this.owner));
          }
          else {
            this.messageService.alert("Alert", "", response.message, false)
          }
          loader.dismiss();
        }, (err) => {
          this.messageService.alert("Alert", "Error",
            "Something Went Wrong. Please Try Again!", false);
          loader.dismiss();
        });
      });
  }

}

b. home

home-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomePage } from './home.page';

const routes: Routes = [
  {
    path: '',
    component: HomePage,
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class HomePageRoutingModule {}
home.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { HomePage } from './home.page';

import { HomePageRoutingModule } from './home-routing.module';


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    HomePageRoutingModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

home.page.html

<ion-header [translucent]="true">
  <ion-toolbar mode="ios">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Lucasology</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="chevron-down-circle-outline"
      pullingText="Pull to refresh"
      refreshingSpinner="circles"
      refreshingText="Refreshing...">
    </ion-refresher-content>
  </ion-refresher>

  <ion-card *ngFor="let p of posts" (click)="openPost(p)">
    <img src="https://lucasology.com/Uploads/BlogImages/{{p.thumbnail}}" />
    <ion-card-header>
      <ion-card-subtitle>{{p.category}}</ion-card-subtitle>
      <ion-card-title>{{p.title}}</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      {{p.preview}}...
    </ion-card-content>
  </ion-card>

  <ion-infinite-scroll threshold="5%" (ionInfinite)="loadMore($event)">
    <ion-infinite-scroll-content loadingSpinner="dots" loadingText="Loading more data...">
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>

</ion-content>
home.page.scss
#container {
  text-align: center;

  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

#container strong {
  font-size: 20px;
  line-height: 26px;
}

#container p {
  font-size: 16px;
  line-height: 22px;

  color: #8c8c8c;

  margin: 0;
}

#container a {
  text-decoration: none;
}

home.page.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { HomePage } from './home.page';

describe('HomePage', () => {
  let component: HomePage;
  let fixture: ComponentFixture<HomePage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HomePage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
home.page.ts
import { Component, ViewChild } from '@angular/core';
import { NavController, LoadingController, ModalController, AlertController, MenuController } from '@ionic/angular';
import { Platform } from '@ionic/angular';
import { IonInfiniteScroll } from '@ionic/angular';
import { Router, NavigationExtras } from '@angular/router'

import { MessageService } from '../../services/messageService';
import { PostService } from '../../services/postService';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  @ViewChild(IonInfiniteScroll, { static: false }) infiniteScroll: IonInfiniteScroll;

  public posts: any;
  private pageSize = 4;
  private pageNumber = 0;
  private totalCount = 0;

  constructor(public navCtrl: NavController
    , public loadingCtrl: LoadingController
    , public menuCtrl: MenuController
    , public modalCtrl: ModalController
    , public alertCtrl: AlertController
    , public messageService: MessageService
    , private postService: PostService
    , private plform: Platform
    , private router: Router) {
    this.menuCtrl.enable(true, "mainMenu");
    this.posts = [];
    // this.isItemAvailable = false;
    this.getPublicPosts(this.pageSize, this.pageNumber);
  }

  doRefresh(event) {
    console.log('Begin async operation');

    setTimeout(() => {
      this.getPublicPosts(this.pageSize, this.pageNumber);
      console.log('Async operation has ended');
      event.target.complete();
    }, 100);
  }

  async getPublicPosts(pageSize, pageNumber) {
    if (this.pageNumber == 0) {
      const loader = await this.loadingCtrl.create({
        spinner: 'dots',
        duration: 5000,
        message: 'Please wait...',
        translucent: true,
        cssClass: 'custom-class custom-loading'
      });

      await loader.present().then(() => {
        this.postService.getPublicPosts(pageSize, pageNumber).subscribe((response) => {
          if (response.success) {
            this.posts = this.posts.concat(response.data);
          }
          else {
            this.messageService.alert("Alert", "", response.message, false)
          }
          loader.dismiss();
        }, (err) => {
          this.messageService.alert("Alert", "Error",
            "Something Went Wrong. Please Try Again!", false);
          loader.dismiss();
        });
      });
    }
    else {
      this.postService.getPublicPosts(pageSize, pageNumber).subscribe((response) => {
        if (response.success) {
          this.posts = this.posts.concat(response.data);
        }
        else {
          this.messageService.alert("Alert", "", response.message, false)
        }
      }, (err) => {
        this.messageService.alert("Alert", "Error",
          "Something Went Wrong. Please Try Again!", false);
      });
    }
  }

  loadMore(event) {
    this.pageNumber++;
    this.getPublicPosts(this.pageSize, this.pageNumber);
    setTimeout(() => {
      console.log('Done');
      event.target.complete();

      // App logic to determine if all data is loaded
      // and disable the infinite scroll
      if (this.posts.length == this.totalCount) {
        event.target.disabled = true;
      }
    }, 500);
  }

  openPost(post) {
    this.postService.getPostDetail(post.id).subscribe((response) => {
      let navigationExtras: NavigationExtras = {
        state: {
          post: response.data
        }
      };

      this.router.navigate(['post-detail'], navigationExtras);
    });
  }

}

c. menu

menu-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { MenuPage } from './menu.page';

const routes: Routes = [
  {
    path: '',
    component: MenuPage,
    children: [
      {
        path: 'tabs',
        loadChildren: () => import('../tabs/tabs.module').then( m => m.TabsPageModule)
      },
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class MenuPageRoutingModule {}
menu.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { MenuPageRoutingModule } from './menu-routing.module';

import { MenuPage } from './menu.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    MenuPageRoutingModule
  ],
  declarations: [MenuPage]
})
export class MenuPageModule {}
menu.page.html
<ion-app>
  <ion-menu contentId="content">
    <ion-header>
      <ion-toolbar color="primary">
        <ion-title>Menu</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <ion-list>
        <ion-menu-toggle auto-hide="false" *ngFor="let p of pages">
          <ion-item [routerLink]="p.url" routerDirection="root"
            [class.active-item]="selectPath.startsWith(p.url)">
          <ion-icon [name]="p.icon" slot="start"></ion-icon>
          <ion-label>{{p.title}}</ion-label>
        </ion-item>
        </ion-menu-toggle>
        <!-- <ion-menu-toggle auto-hide="false">
          <ion-item (click)="logout()">
            <ion-icon name="log-out-outline" slot="start"></ion-icon>
            <ion-label>Logout</ion-label>
          </ion-item>
        </ion-menu-toggle> -->
      </ion-list>
    </ion-content>
  </ion-menu>

  <ion-router-outlet id="content" main></ion-router-outlet>
</ion-app>
menu.page.scss
.active-item {
    border-left: 4px solid var(--ion-color-primary);
}
menu.page.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { MenuPage } from './menu.page';

describe('MenuPage', () => {
  let component: MenuPage;
  let fixture: ComponentFixture<MenuPage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MenuPage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(MenuPage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
menu.page.ts
import { Component, OnInit } from '@angular/core';
import { Router, RouterEvent } from '@angular/router';

import { NavController } from '@ionic/angular';

@Component({
  selector: 'app-menu',
  templateUrl: './menu.page.html',
  styleUrls: ['./menu.page.scss'],
})
export class MenuPage implements OnInit {
  pages = [
    {
      title: 'Home',
      url: '/menu/tabs/home',
      icon: 'home'
    },
    {
      title: 'About',
      url: '/menu/tabs/about',
      icon: 'person-circle-outline'
    }
  ];
  selectPath = '';
  constructor(private router: Router) { 
      this.router.events.subscribe((event: RouterEvent) => {
        if(event && event.url) {
          this.selectPath = event.url;
        }
      });
    }

  ngOnInit() {
  }

}

d. post-detail

post-detail-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PostDetailPage } from './post-detail.page';

const routes: Routes = [
  {
    path: '',
    component: PostDetailPage
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class PostDetailPageRoutingModule {}
post-detail.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { PostDetailPageRoutingModule } from './post-detail-routing.module';

import { PostDetailPage } from './post-detail.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    PostDetailPageRoutingModule
  ],
  declarations: [PostDetailPage]
})
export class PostDetailPageModule {}

post-detail.page.html

<ion-header>
  <ion-toolbar mode="md">
    <ion-buttons slot="start">
      <ion-back-button></ion-back-button>
    </ion-buttons>
    <ion-title>{{post.title}}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card>
    <img src="https://lucasology.com/Uploads/BlogImages/{{post.thumbnail}}" />
    <ion-card-header>
      <ion-card-subtitle>{{post.category}}</ion-card-subtitle>
      <ion-card-title>{{post.title}}</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <span [innerHTML]="post.content"></span>
    </ion-card-content>
  </ion-card>
</ion-content>

post-detail.page.scss

.container {
    width: 100000px !important;
}

post-detail.page.spect.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { PostDetailPage } from './post-detail.page';

describe('PostDetailPage', () => {
  let component: PostDetailPage;
  let fixture: ComponentFixture<PostDetailPage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ PostDetailPage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(PostDetailPage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
post-detail.page.ts
import { Component, OnInit } from '@angular/core';
import {ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-post-detail',
  templateUrl: './post-detail.page.html',
  styleUrls: ['./post-detail.page.scss'],
})
export class PostDetailPage implements OnInit {

  post: any;
  postcontent: string;
 
  constructor(private route: ActivatedRoute, private router: Router) {
    this.route.queryParams.subscribe(params => {
      if (this.router.getCurrentNavigation().extras.state) {
        this.post = this.router.getCurrentNavigation().extras.state.post;
        this.postcontent = this.post.content;
      }
    });
  }
 
  ngOnInit() { }

}

e. tabs

tabs-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: '',
    component: TabsPage,
    children: [
      {
        path: 'home',
        loadChildren: () => import('../home/home.module').then( m => m.HomePageModule)
      },
      {
        path: 'about',
        loadChildren: () => import('../about/about.module').then( m => m.AboutPageModule)
      }
    ]
  }
];

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

tabs.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { TabsPageRoutingModule } from './tabs-routing.module';

import { TabsPage } from './tabs.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    TabsPageRoutingModule
  ],
  declarations: [TabsPage]
})
export class TabsPageModule {}

tabs.page.html

<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon name="home"></ion-icon>
      <ion-label>Home</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="about">
      <ion-icon name="person-circle-outline"></ion-icon>      
      <ion-label>About</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

tab.page.scss: leave it blank

tab.page.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { TabsPage } from './tabs.page';

describe('TabsPage', () => {
  let component: TabsPage;
  let fixture: ComponentFixture<TabsPage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ TabsPage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(TabsPage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

tabs.page.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-tabs',
  templateUrl: './tabs.page.html',
  styleUrls: ['./tabs.page.scss'],
})
export class TabsPage implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

3. services folder

a. apiService.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Observer } from 'rxjs';
import { NavController, Config } from '@ionic/angular';

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { IResponse } from "../models/interface";

@Injectable()
export class RestapiService {
    apiUrl = 'https://lucasology.com/api/';

    constructor(public config: Config,
      public navCtrl: NavController,
      private httpClient: HttpClient) {
    }

    postRequest(url, postParams): Observable<Response>  {
      return Observable.create((observer: Observer<any>) => {
        this.generateOptions().then((data) => {
          this.httpClient.post(this.apiUrl + url, postParams, data.data).subscribe((response) => {
              observer.next(response);
          }, error => {
            observer.error(error);
            this.navCtrl.navigateRoot('/pin');
          })
        });
      });
    }

    postURLRequest(url): Observable<Response>  {
      return Observable.create((observer: Observer<any>) => {
        this.generateOptions().then((data) => {
          this.httpClient.post(this.apiUrl + url, null, data.data).subscribe((response) => {
              observer.next(response);
          }, error => {
            observer.error(error);
            this.navCtrl.navigateRoot('/pin');
          })
        });
      });
    }

    getRequest(url): Observable<Response>  {
      return Observable.create((observer: Observer<any>) => {
        this.generateOptions().then((data) => {
          this.httpClient.get(this.apiUrl + url, data.data).subscribe((response) => {
              observer.next(response);
          }, error => {
            observer.error(error);
            this.navCtrl.navigateRoot('/error');
          })
        })
      });
    }

    generateOptions(): Promise<IResponse> {
      return new Promise((resolve, reject) => {
        let headers = new HttpHeaders({
            'Access-Control-Allow-Origin': '*'
        });
        const httpOptions = { headers: headers };
        resolve({
            data: httpOptions, success: true,
            message: "ok"
        });
      })
  };

  private handleError() {
      return { data: [], success: false, message: "Internal Database Error" };
  }
}

b. messageService.ts

import { Injectable } from '@angular/core';
import { ToastController, AlertController, NavController } from '@ionic/angular';

@Injectable()
export class MessageService {

    public constructor(private toastCtrl: ToastController, 
      private alertCtrl: AlertController,
      private navCtrl: NavController) { }

    public async toast(message: string, position: string = "top", duration: number = 2000) {
        const toast = await this.toastCtrl.create({
            header: 'Toast header',
            message: message,
            position: "top",
            buttons: [
              {
                side: 'start',
                icon: 'star',
                text: 'Favorite',
                handler: () => {
                  console.log('Favorite clicked');
                }
              }, {
                text: 'Done',
                role: 'cancel',
                handler: () => {
                  console.log('Cancel clicked');
                }
              }
            ]
          });
          toast.present();
    }

    public async alert(title: string = "Alert", subTitle: string = "", message: string, enableBackdropDismiss: boolean = false) {
        const alert = await this.alertCtrl.create({
            header: title,
            subHeader: subTitle,
            message: message,
            buttons: ['OK']
        });

        await alert.present();
    }

    public async alertRedirect(title: string = "Alert", subTitle: string = "", message: string, 
                              redirectUrl: string, enableBackdropDismiss: boolean = false) {
        const alert = await this.alertCtrl.create({
            header: title,
            subHeader: subTitle,
            message: message,
            buttons: [{
              text: 'OK',
              handler:  () => {
                this.navCtrl.navigateRoot(redirectUrl)
              }
            }]
        });

        await alert.present();
    }
}

c. ownerService.ts

import { Injectable } from '@angular/core';
import { RestapiService } from './apiService';
import { Observable } from "rxjs";
import { Observer } from "rxjs";
import { IResponse } from "../models/interface";
// import 'rxjs/add/operator/map';

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

/*
  Generated class for the Restapi provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular 2 DI.
*/
@Injectable()
export class OwnerService {

    constructor(
        private httpClient: HttpClient,
        private restapiService: RestapiService) {
    }

    getAboutMe(): Observable<IResponse> {
        return Observable.create((observer: Observer<IResponse>) => {
            this.restapiService.getRequest('AboutMe/Get')
            .subscribe(async (response) => {
                observer.next(<IResponse>JSON.parse(await JSON.stringify(response)));
            }, (error) => {
                observer.error(error);
            })
        })
    }

}

d. blogService.ts

import { Injectable } from '@angular/core';
import { Subject } from "rxjs";
import { RestapiService } from './apiService';
import { Observable } from "rxjs";
import { Observer } from "rxjs";
import { IResponse } from "../models/interface";
// import 'rxjs/add/operator/map';

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

/*
  Generated class for the Restapi provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular 2 DI.
*/
@Injectable()
export class PostService {

    constructor(
        private httpClient: HttpClient,
        private restapiService: RestapiService) {
    }

    getPublicPosts(pageSize, pageNumber): Observable<IResponse> {
        return Observable.create((observer: Observer<IResponse>) => {
            this.restapiService.getRequest('Blog/GetPublicPosts?pageNumber=' + pageNumber + '&pageSize=' + pageSize)
            .subscribe(async (response) => {
                observer.next(<IResponse>JSON.parse(await JSON.stringify(response)));
            }, (error) => {
                observer.error(error);
            })
        })
    }

    getPostDetail(id): Observable<IResponse> {
        return Observable.create((observer: Observer<IResponse>) => {
            this.restapiService.getRequest('Blog/GetPostDetails?id=' + id)
            .subscribe(async (response) => {
                observer.next(<IResponse>JSON.parse(await JSON.stringify(response)));
            }, (error) => {
                observer.error(error);
            })
        });
    }

}

4. root files

a. app-routing.module.ts

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'tabs',
    loadChildren: () => import('./pages/tabs/tabs.module').then( m => m.TabsPageModule)
  },
  {
    path: 'menu',
    loadChildren: () => import('./pages/menu/menu.module').then( m => m.MenuPageModule)
  },
  {
    path: 'post-detail',
    loadChildren: () => import('./pages/post-detail/post-detail.module').then( m => m.PostDetailPageModule)
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
b. app.component.html
<ion-app>
  <ion-router-outlet></ion-router-outlet>
</ion-app>
c. app.component.scss: leave it blank
d. app.component.spec.ts
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';

import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';

describe('AppComponent', () => {

  let statusBarSpy;
  let splashScreenSpy;
  let platformReadySpy;
  let platformSpy;

  beforeEach(async(() => {
    statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
    splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
    platformReadySpy = Promise.resolve();
    platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });

    TestBed.configureTestingModule({
      declarations: [AppComponent],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        { provide: StatusBar, useValue: statusBarSpy },
        { provide: SplashScreen, useValue: splashScreenSpy },
        { provide: Platform, useValue: platformSpy },
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it('should initialize the app', async () => {
    TestBed.createComponent(AppComponent);
    expect(platformSpy.ready).toHaveBeenCalled();
    await platformReadySpy;
    expect(statusBarSpy.styleDefault).toHaveBeenCalled();
    expect(splashScreenSpy.hide).toHaveBeenCalled();
  });

  // TODO: add more tests!

});
e. app.component.ts
import { Component } from '@angular/core';

import { NavController, Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private navCtrl: NavController
  ) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      // let status bar overlay webview
      this.statusBar.overlaysWebView(false);

      // set status bar to white
      this.statusBar.backgroundColorByHexString('#000000');

      // this.statusBar.styleDefault();
      this.splashScreen.hide();

      this.navCtrl.navigateRoot('/menu/tabs/home');
    });
  }
}
f. app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

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

import { RestapiService } from './services/apiService';
import { OwnerService } from './services/ownerService';
import { PostService } from './services/postService';
import { MessageService } from './services/messageService';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    HttpClientModule, 
    IonicModule.forRoot(), 
    AppRoutingModule
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    RestapiService,
    PostService,
    OwnerService,
    MessageService
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
Woops! Done ... Feeeewsss! Sorry for the long post. Now, you can run below command in terminal to test the app on both Anroid and iOS:
ionic serve -l



There are some benefits of keeping both UI and API parts in the same place for small projects. In this article, I will explain how I did to deploy Angular Web and ASP .Net Core API in the same folder ...

Using cherry-pick to select specific commits for your Pull Request.1. Create a new branch based on the target of the Pull Requestgit branch cherry-branch origin/master2. Switch to a new branchgit chec ...

I got CORS error after publishing my API and Angular app to IIS even though CORS is enabled and the origins of the Angular app is added. Below is how I resolved this issue.Just simple, make sure you s ...

After deployment Angular and API on IIS, it's working fine unless I refresh the page. Once refreshed, the web encountered 404 error. In this article, I will explain how to resolve this.Since Angular i ...

In Object-Oriented Programming, S.O.L.I.D refers to the first five design principle for the purpose of making software designs more understandable, flexible, and maintainable. The principles was first ...

1. The Situation:Error Message:&nbsp;Pulse Secure Application failed to load Java. Please install correct JRE version.Description: This issue happens when I'm using a M1 Mac with a correct version of ...

Below is how to decrypt/convert a Hex string value into text using VB.Net:Decrypting Hex string value to string in VB.Net Function HexToString(ByVal hex As String) As String Dim text As New Sy ...

After a month of publishing on Google Play, Jungle Words has made it to the Top Android Games To Try Out In April 2021. Please check it out!&nbsp;GameKeys.netGameKeys is a website which introduces gam ...

Centering HTML elements is an important aspect for anything involving designing with CSS. There are various methods of centering things, however, it also depends on the elements that you are working w ...