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
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
- Shows all articles from lucasology.com
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:
- .Net MVC Core web application: Build An N-Tier Structured Web Application With .Net MVC Core
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>
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 {}
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>
#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(); }); });
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 {}
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 {}
<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>
.active-item { border-left: 4px solid var(--ion-color-primary); }
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(); }); });
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 {}
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(); }); });
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 { }
<ion-app> <ion-router-outlet></ion-router-outlet> </ion-app>
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! });
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'); }); } }
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 {}
ionic serve -l