Ionic with Firebase for Hacker News Apps

To describe the app’s requirements, we list the main user stories as below.

  1. View top stories – Users can view a list of top stories on Hacker News and view the page of each story.
  2. View comments – For each story, users can view its comments. For each comment, users can also view replies to that comment.
  3. Add stories to favorites – Users can add a story to favorites and see a list of all favorite stories.
  4. Share stories – Users can share stories to the social network.
ionic start hacker_news_app sidemenu

$ ionic platform add ios --save
$ ionic platform add android --save

 

Step 1: Listing the Stories

We start implementing the first user story that lists Hacker News top stories. We are going to cover the following topics in this long chapter. Use the list component to show top stories and test with Jasmine and Karma. Services to load top stories.

  1. Firebase basics and Hacker News API.
  2. Infinite scrolling and pull-to-refresh.
  3. Loading indicators and error handling.

Define the Model
The app starts, the user is presented with a list of top stories on Hacker News. The user can see basic information on each story on the list, including title, URL, author, published time, and score. The information for each story should be declared in the model.

A model in programming is a generic concept, and you may have come across it with common patterns like Model-View-Controller (or MVC). Depending on the context, the exact definition of a model may vary, but in general, a model is used to store or represent data.
We declare the model as a TypeScript interface in. Here we use a more general model name Item instead of Story because comments are also items and can share the same model.

export interface Item {
	id: number;
	title: string;
	url: string;
	by: string;
	time: number;
	score: number;
}

The list of top stories can be represented as an array of items, that is, Item[]. However, we define a type Items in below to represent a list of items.

import { Item } from './Item';
export type Items = Item[];

In app we will create the folder called app/model/ and add two files as item.ts and items.ts file. Add the following code in item.ts file

export interface Item {
  id: number;
  title: string;
  url: string;
  by: string;
  time: number;
  score: number;
  text?: string;
  descendants?: number;
  kids?: number[];
  isFavorite?: boolean;
}

And add the following code in items.ts

import { Observable } from 'rxjs';
import { Item } from './Item';

export interface Items {
  refresh?: boolean;
  offset: number;
  limit: number;
  total?: number;
  results: Observable<Item>[];
  loading?: boolean;
}

 

Step 2: Display a List of Items through component

After defining the model Item and learning the component ion-list, we are now ready to display a list of items in the app. We need to create a component for the list of items and another component for each item in the list.

$ ionic g component Items
$ ionic g component Item

Ionic CLI creates the necessary files for a component, including TypeScript file, HTML template file, and Sass file. For the generated component, these files are created under the related subdirectory of the src/components directory.

Add or modify the app/components/item/item.ts file. 

import { Component, Input } from '@angular/core';
import { Item } from '../../model/item';

@Component({
  selector: 'item',
  templateUrl: 'item.html'
})
export class ItemComponent {
  @Input() item: Item;
}

Add or modify the app/components/item/item.html file. 

<div>
  <h2 class="title">{{ item.title }}</h2>
  <div> 
    <span><ion-icon name="bulb"></ion-icon>{{ item.score }}</span>
    <span>
      <ion-icon name="person"></ion-icon>
      {{ item.by }}
    </span>
    <span>
      <ion-icon name="time"></ion-icon>
      {{ item.time | timeAgo }}
    </span>
  </div>
  <div>
    <span>
      <ion-icon name="link"></ion-icon>
      {{ item.url }}
    </span>
  </div>
</div>

Important: The timeAgo used in {{ item.time | timeAgo }} is an Angular 2 pipe to transform the timestamp item.time into a human-readable text, like 1 minute ago or 5 hours ago. The implementation of TimeAgoPipe uses the taijinlee/humanize (https://github.com/taijinlee/humanize) library to generate a human-readable text. So we need to install the humanize library to our projects as

ionic generate pipe TimeAgoPipe
npm install humanize --save

Some example of humanize used, 

humanize.date('Y-m-d'); // 'yyyy-mm-dd'
humanize.filesize(1234567890); // '1.15 Gb'

Add/Modify the following code in src/app/pipes/time-ago-pipe.ts 

import { Pipe, PipeTransform } from '@angular/core';
import * as humanize from 'humanize';

@Pipe({
  name: 'timeAgo',
})
export class TimeAgoPipe implements PipeTransform {
  
  transform(time: number) {
    return humanize.relativeTime(time);
  }
}

 

Items Component

The ItemsComponent is used to render a list of items. Add the following code in src/components/items/items.ts, the selector for this component is items. It also has the property items that are bound to the value of the property items from its parent component.

import { Component, Input } from '@angular/core';
import { Items } from '../../model/Items';

@Component({
  selector: 'items',
  templateUrl: 'items.html',
})
export class ItemsComponent {
  @Input() items: Items;
}

 

Add or modify the app/components/item/items.html file. 

<ion-list *ngIf="items.length > 0">
  <ion-item *ngFor="let item of items">
    <item [item]="item"></item>
  </ion-item>
</ion-list>
<p *ngIf="items.length === 0">
  No items.
</p>

 

Step 3: Items Loading Service:

In Angular apps, code logic that deals with external resources should be encapsulated in services.
ItemService in below has a single method load(offset, limit) to load a list of items. Because there can be many items, the load() method only loads a subset of items. Parameters offset and limit are used for pagination: offset specifies the position of the first loaded item in the whole items list, limit specifies the number of loaded items. For example, load(0, 10) means loading the first 10 items. Add the following code in src/providers/ItemService.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Items } from '../model/Items';

@Injectable()
export class ItemService {
  load(offset: number, limit: number): Observable<Items> {
    return Observable.of({
      offset: 0,
      limit: 0,
      total: 0,
      results: [],
    });
  }
}

ItemService uses decorator factory Injectable, so it’ll be registered to Angular 2’s injector and available for dependency injection to other components

The return type of the load() method is Observable<Items>. Items are the model type we defined already and now we have to update the Items model to add more information related to pagination. The Items type now has both offset and limit properties that match the parameters in the load() method. It also has the property total that represents the total number of items. The property total is optional, because in some cases the total number may not be available. The property results represent the actual array of items.

Updated Item model 

import { Observable } from 'rxjs';
import { Item } from './Item';

export interface Items {
  refresh?: boolean;
  offset: number;
  limit: number;
  total?: number;
  results: Observable<Item>[];
  loading?: boolean;
}

Step 4: Adding the page – Top Stories Page 
Now we can create a new page in the Ionic app to show top stories using ItemsComponent and ItemService. TopStoriesPage class has property items of type Items. The value of this property is passed to the ItemsComponent for rendering.

ionic g page topStories

Add the following line of code in src/pages/top-stories/top-stories.ts file

import { Component, OnInit, OnDestroy } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Subscription } from "rxjs";
import { Items } from '../../model/Items';
import { ItemService } from '../../providers/ItemService';

@Component({
selector: 'page-top-stories',
templateUrl: 'top-stories.html'
})
export class TopStoriesPage implements OnInit, OnDestroy {
  items: Items;
  subscription: Subscription;
  constructor(public navCtrl: NavController, private itemService: ItemService) {}
  
  ngOnInit(): void {
    this.subscription = this.itemService.load(0, 10).subscribe(items =>
      this.items = items
    );
  }

  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}

In the ngOnInit() method, the method load() of ItemService is invoked. When the loading is finished, loaded items is set as the value of the property items. TopStoriesPage class also implements the OnDestroy interface. In the method ngOnDestroy(), we use the method unsubscribe() of the Observable subscription to make sure resources are released.

 

Add the following line of code in src/pages/top-stories/top-stories.html file

<ion-header>
  <ion-navbar>
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Top Stories</ion-title>
  </ion-navbar>
</ion-header>
<ion-content padding>
  <items *ngIf="items" [items]="items"></items>
</ion-content>

Important: In above highlight code <items *ngIf=”items” [items]=”items”></items> creates the ItemsComponent and binds the value of the property items to the items in TopStoriesPage class. Because loading items are asynchronous, the value of items is null until the loading is finished successfully. The usage of directive ngIf is to make sure the ItemsComponent is only created after items are loaded.

Step 5: Creating Database – Firebase

npm install angularfire2 --save