CHAPTER 1. Getting Started
Starting an App |
CHAPTER 3. Web Components and Stencil
Example 1. CSS Tricks tutorial on Web Components
CHAPTER 4. Basic App Structure
Understanding the Basic App Structure
$ ionic start hacker-news-app-v4 blank --package-id=io.vividcode.ionic4.hnc --type angular --cordova
$ ionic cordova platform add android --save
CHAPTER 5. List Stories
Define the Model
Listing 5-1. Item model
export interface Item {
id: number;
title: string;
url: string;
by: string;
time: number;
score: number;
}
Listing 5-2. Items model
import { Item } from './Item';
export type Items = Item[];
List Component
Listing 5-4. List with header and dividers
<ion-list>
<ion-list-header>
Items
</ion-list-header>
<ion-item>
Item 1
</ion-item>
<ion-item-divider>
<ion-button slot="start">Start</ion-button>
<ion-label>Divider</ion-label>
<ion-icon slot="end" name="book"></ion-icon>
</ion-item-divider>
<ion-item>
Item 2
</ion-item>
</ion-list>
Grouping of Items
Listing 5-5. Grouping of items
<ion-item-group>
<ion-item-divider>A</ion-item-divider>
<ion-item>Alex</ion-item>
<ion-item>Amber</ion-item>
</ion-item-group>
<ion-item-group>
<ion-item-divider>B</ion-item-divider>
<ion-item>Bob</ion-item>
<ion-item>Brenda</ion-item>
</ion-item-group>
Icons
Listing 5-6. List with icons
<ion-list>
<ion-item>
<ion-icon name="book" slot="start"></ion-icon>
Book
</ion-item>
<ion-item>
<ion-icon name="build" is-active="false" slot="start">
</ion-icon>
Build
<ion-icon name="build" slot="end"></ion-icon>
</ion-item>
<ion-item>
<ion-icon ios="ios-happy" md="md-sad" slot="end">
</ion-icon>
Happy or Sad
</ion-item>
</ion-list>
Avatars
Listing 5-7. List with avatars
<ion-list>
<ion-item>
<ion-avatar slot="start">
<img src="http://placehold.it/60?text=A">
</ion-avatar>
Alex
</ion-item>
<ion-item>
<ion-avatar slot="start">
<img src="http://placehold.it/60?text=B">
</ion-avatar>
Bob
</ion-item>
<ion-item>
<ion-avatar slot="start">
<img src="http://placehold.it/60?text=D">
</ion-avatar>
David
</ion-item>
</ion-list>
Thumbnails
Listing 5-8. List with thumbnails
<ion-list>
<ion-item>
<ion-thumbnail slot="start">
<img src="http://placehold.it/100x60?text=F1">
</ion-thumbnail>
Apple
</ion-item>
<ion-item>
<ion-thumbnail slot="start">
<img src="http://placehold.it/100x60?text=F2">
</ion-thumbnail>
Banana
</ion-item>
<ion-item>
<ion-thumbnail slot="start">
<img src="http://placehold.it/100x60?text=F3">
</ion-thumbnail>
Orange
</ion-item>
</ion-list>
Display a List of Items
$ ng g module components --flat false
Listing 5-9. Use CUSTOM_ELEMENTS_SCHEMA in the module
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ComponentsModule { }
Item Component
$ ng g component components/item --flat false
Listing 5-10. Item component
import { Component, Input } from '@angular/core';
import { Item } from '../../model/Item';
@Component({
selector: 'item',
templateUrl: 'item.html',
})
export class ItemComponent {
@Input() item: Item;
}
Listing 5-11. Template of the item component
<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 }} ago
</span>
</div>
<div>
<span>
<ion-icon name="link"></ion-icon>
{{ item.url }}
</span>
</div>
</div>
Listing 5-12. timeAgo pipe
import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';
@Pipe({
name: 'timeAgo'
})
export class TimeAgoPipe implements PipeTransform {
transform(time: number): string {
return moment.duration(moment().diff(moment(time * 1000))).
humanize();
}
}
Listing 5-13. Styles of the item component
:host {
width: 100%;
}
.title {
color: #488aff;
font-size: 18px;
font-weight: 500;
margin-bottom: 5px;
}
.link {
font-size: 14px;
}
div {
margin: 1px;
}
ion-icon {
margin-right: 2px;
}
div > span:not(:last-child) {
padding-right: 10px;
}
Items Component
Listing 5-14. Items component
import { Component, Input } from '@angular/core';
import { Items } from '../../models/items';
import { Item } from '../../models/item';
@Component({
selector: 'app-items',
templateUrl: './items.component.html',
styleUrls: ['./items.component.scss']
})
export class ItemsComponent {
@Input() items: Items;
}
Listing 5-15. Template of items component
<ion-list>
<ion-item *ngFor="let item of items">
<app-item [item]="item"></app-item>
</ion-item>
</ion-list>
Empty List
Listing 5-16. Show empty list
<ion-list *ngIf="items && items.length > 0">
<ion-item *ngFor="let item of items">
<app-item [item]="item"></app-item>
</ion-item>
</ion-list>
<p *ngIf="items && items.length === 0">
No items.
</p>
<p *ngIf="!items">
Loading...
</p>
Unit Tests of Components
Testing Items Component
Listing 5-20. items.components.spec.ts
import { async, ComponentFixture } from '@angular/core/testing';
import { ItemsComponent } from './items.component';
import { ItemComponent } from '../item/item.component';
import { TimeAgoPipe } from '../time-ago/time-ago.pipe';
import { TestUtils } from '../../../testing/test-utils';
import { By } from '@angular/platform-browser';
describe('ItemsComponent', () => {
let component: ItemsComponent;
let fixture: ComponentFixture<ItemsComponent>;
beforeEach(async(() => {
TestUtils.beforeEachCompiler([ItemsComponent,
ItemComponent, TimeAgoPipe])
.then(compiled => {
fixture = compiled.fixture;
component = compiled.instance;
});
}));
it('should display a list of items', () => {
component.items = [{
id: 1,
title: 'Test item 1',
url: 'http://www.example.com/test1',
by: 'user1',
time: 1478576387,
score: 242,
}, {
id: 2,
title: 'Test item 2',
url: 'http://www.example.com/test2',
by: 'user2',
time: 1478576387,
score: 100,
}];
fixture.detectChanges();
const debugElements = fixture.debugElement.queryAll(By.css('h2'));
expect(debugElements.length).toBe(2);
expect(debugElements[0].nativeElement.textContent).toContain('Test item 1');
expect(debugElements[1].nativeElement.textContent).toContain('Test item 2');
});
it('should display no items', () => {
component.items = [];
fixture.detectChanges();
const debugElement = fixture.debugElement.query(By.css('p'));
expect(debugElement).not.toBeNull();
expect(debugElement.nativeElement.textContent).toContain('No items');
});
});
Listing 5-21. TestUtils
import { TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
export class TestUtils {
static beforeEachCompiler(components: Array<any>, providers:
Array<any> = []): Promise<{fixture: any, instance: any}> {
return TestUtils.configureIonicTestingModule(components, providers)
.compileComponents().then(() => {
const fixture: any = TestBed.createComponent(components[0]);
return {
fixture,
instance: fixture.componentInstance,
};
});
}
static configureIonicTestingModule(components: Array<any>,
providers: Array<any> = []): typeof TestBed {
return TestBed.configureTestingModule({
declarations: [
...components,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
...providers,
],
imports: [
FormsModule,
IonicModule,
],
});
}
}
Items Loading Service
$ ng g module services --flat false
Listing 5-22. ItemService
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Items } from '../../models/items';
@Injectable()
export class ItemService {
load(offset: number, limit: number): Observable<Items> {
return of({
offset: 0,
limit: 0,
total: 0,
results: [],
});
}
Listing 5-23. Updated Item model
import { Item } from './Item';
export interface Items {
offset: number;
limit: number;
total?: number;
results: Item[];
}
Top Stories Page
Listing 5-24. Generate modules and components
$ ng g module top-stories --routing
$ ng g component top-stories -m top-stories
Listing 5-25. top-stories.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Items } from '../../models/items';
import { ItemService } from '../../services/item/item.service';
@Component({
selector: 'app-top-stories',
templateUrl: './top-stories.component.html',
styleUrls: ['./top-stories.component.scss']
})
export class TopStoriesComponent implements OnInit, OnDestroy {
items: Items;
private subscription: Subscription;
constructor(private itemService: ItemService) { }
ngOnInit() {
this.subscription = this.itemService.load(0, 10).
subscribe(items => this.items = items);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
Listing 5-26. top-stories.html
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Top Stories</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<app-items [items]="items"></app-items>
</ion-content>
</ion-app>
Test
Items Loading Service Mock
Listing 5-27. ItemServiceMock
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import * as range from 'lodash.range';
import { Items } from '../model/Items';
import { Item } from '../model/Item';
import { ItemService } from '../services/ItemService';
@Injectable()
export class ItemServiceMock extends ItemService {
load(offset?: number, limit?: number): Observable<Items> {
const results: Item[] = range(offset, offset + limit).
map(index => ({
id: index,
title: `Item ${index + 1}`,
url: `http://www.example.com/item${index}`,
by: `demo`,
time: new Date().getTime() / 1000,
score: index,
}));
return Observable.of({
offset,
limit,
total: offset + limit,
results,
});
}
}
Test Suite
Listing 5-28. top-stories.spec.ts
import { ComponentFixture, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TestUtil } from '../../test';
import { TopStoriesComponent } from './top-stories.component';
import { ItemsComponent } from '../../components/items/items.component';
import { ItemComponent } from '../../components/item/item.component';
import { TimeAgoPipe } from '../../pipes/TimeAgoPipe';
import { ItemService } from '../../services/ItemService';
import { ItemServiceMock } from '../../testing/ItemServiceMock';
let fixture: ComponentFixture<TopStoriesComponent> = null;
let component: any = null;
describe('top stories page', () => {
beforeEach(async(() => TestUtils.beforeEachCompiler(
[TopStoriesComponent, ItemsComponent, ItemComponent, TimeAgoPipe],
[{provide: ItemService, useClass: ItemServiceMock}]
).then(compiled => {
fixture = compiled.fixture;
component = compiled.instance;
})));
it('should display a list of 10 items', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let debugElements = fixture.debugElement.queryAll(By.css('h2'));
expect(debugElements.length).toBe(10);
expect(debugElements[0].nativeElement.textContent).toContain('Item 1');
expect(debugElements[1].nativeElement.textContent).toContain('Item 2');
});
}));
});
Firebase Basics
Firebase JavaScript SDK
Setup
<script src="https://www.gstatic.com/firebasejs/5.9.2/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "AIzaSyDVolqj1aX7IVpMsM4TPneXowef18_j-Vk",
authDomain: "ionic4-code.firebaseapp.com",
databaseURL: "https://ionic4-code.firebaseio.com",
projectId: "ionic4-code",
storageBucket: "ionic4-code.appspot.com",
messagingSenderId: "251411004722"
};
firebase.initializeApp(config);
</script>
Read Data
let database = firebase.database();
let ref = database.ref('products');
Listing 5-31. Reading data
ref.on('value', function(snapshot) {
console.log(snapshot.val());
});
Listing 5-32. Remove event listeners
ref.off('value', valueCallback); // Remove a single listener
ref.off('value'); // Remove all listeners of the event 'value'
ref.off(); // Remove all listeners for all events
Listing 5-33. Use events
let ref = database.ref('products');
ref.on('child_added', function(snapshot) {
console.log('product added: ' + snapshot.val().name);
});
ref.on('child_removed', function(snapshot) {
console.log('product removed: ' + snapshot.key);
});
Write Data
Listing 5-34. Writing data
let ref = database.ref('products');
ref.child('00001').set({
"name": "New iPhone 6s plus",
"price": 699.99
});
ref.child('00001').update({
"price": 639.99
});
Listing 5-35. Pushing data to list
let ref = database.ref('customers');
ref.push({
"firstName": "Bob",
"lastName": "Lee",
"email": "bob@example.com"
});
ref.push().set({
"firstName": "Bob",
"lastName": "Lee",
"email": "bob@example.com"
});
Query Data
sort
Listing 5-36. Sort products by price
let ref = database.ref('products');
ref.orderByChild('price');
Filter
Listing 5-37. Filter to only return the first child
let ref = database.ref('products');
ref.orderByChild('price').limitToFirst(1);
Navigation
Listing 5-38. Navigation
let ref = database.ref('products');
ref.child('00001');
// -> path is "/products/00001"
ref.parent;
// -> path is "/"
ref.root;
// -> path is "/"
Hacker News API
AngularFire2
$ npm i firebase @angular/fire
Listing 5-39. AngularFire2 configuration
export const environment = {
production: false,
firebase: {
databaseURL: 'https://hacker-news.firebaseio.com',
},
};
Listing 5-40. AppModule with AngularFire2 config
import { BrowserModule } from '@angular/platform-browser';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MyApp } from './app.component';
import { AngularFireModule } from '@angular/fire';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
MyApp,
],
imports: [
BrowserModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFireDatabaseModule,
],
bootstrap: [MyApp],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: []
})
export class AppModule {}
Listing 5-41. Use AngularFire2 in components
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { AngularFireDatabase } from '@angular/fire/database';
@Component({
selector: 'app-component',
templateUrl: 'app.component.html',
})
export class AppComponent {
items: Observable<any[]>;
constructor(private db: AngularFireDatabase) {
this.items = this.db.list('/items').valueChanges();
}
}
let product = db.object('/products/00001').valueChanges();
<span>{{ (product | async)?.name }}</span>
Hacker News API
Listing 5-42. Sample JSON content of a story
{
"by" : "Thorondor",
"descendants" : 134,
"id" : 9893412,
"kids" : [ 9894173, 9893737, ..., 9893728, 9893803 ],
"score" : 576,
"text" : "",
"time" : 1436987690,
"title" : "The Icy Mountains of Pluto",
"type" : "story",
"url" : "https://www.nasa.gov/image-feature/the-icy-mountains-of-pluto"
}
Implement ItemService
Listing 5-43. ItemService
import { Injectable } from '@angular/core';
import { Observable, combineLatest } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { Items } from '../../models/items';
import { AngularFireDatabase } from '@angular/fire/database';
@Injectable()
export class ItemService {
constructor(private db: AngularFireDatabase) {}
load(offset: number, limit: number): Observable<Items> {
return this.db.list('/v0/topstories')
.valueChanges()
.pipe(
map(ids => ids.slice(offset, offset + limit)),
mergeMap((ids: any[]) => combineLatest(...(ids.map(
id => this.db.object('/v0/item/' + id).valueChanges())))),
map((items: any) => ({
offset,
limit,
total: limit,
results: items,
}))
);
}
}
Alternative Model and Service Implementation
Listing 5-44. Updated model Items
import { Observable } from 'rxjs';
import { Item } from './Item';
export interface Items {
offset: number;
limit: number;
total?: number;
results: Observable<Item>[];
}
Listing 5-45. Updated ItemService
import { Injectable } from '@angular/core';
import * as isEqual from 'lodash.isequal';
import { Observable } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs/operators';
import { Items } from '../../models/items';
import { AngularFireDatabase } from '@angular/fire/database';
@Injectable()
export class ItemService {
constructor(private db: AngularFireDatabase) {}
load(offset: number, limit: number): Observable<Items> {
return this.db.list('/v0/topstories')
.valueChanges()
.pipe(
map(ids => ids.slice(offset, offset + limit)),
distinctUntilChanged(isEqual),
map((ids: any[]) => ids.map(
id => this.db.object('/v0/item/' + id).valueChanges())),
map((items: any) => ({
offset,
limit,
total: limit,
results: items,
}))
);
}
}
Further Improvements
Listing 5-46. Updated ItemService
import { Injectable } from '@angular/core';
import { combineLatest, merge, Observable, Subject } from 'rxjs';
import { filter, map, skip, switchAll, take, withLatestFrom }
from 'rxjs/operators';
import { Items } from '../../models/items';
import { Item } from '../../models/item';
import { AngularFireDatabase } from '@angular/fire/database';
import { Subject } from 'rxjs/Subject';
export interface Query {
refresh?: boolean;
offset: number;
limit: number;
}
@Injectable()
export class ItemService {
private queries: Subject<Query>;
constructor(private db: AngularFireDatabase) {
this.queries = new Subject<Query>();
}
load(query: Query) {
this.queries.next(query);
}
get(): Observable<Items> {
const rawItemIds = this.db.list<number>('/v0/topstories')
.valueChanges();
const itemIds = combineLatest(
rawItemIds,
this.queries
).pipe(
filter(([ids, query]) => query.refresh),
map(([ids, query]) => ids)
);
const selector = ({offset, limit}, ids) =>
combineLatest(...(ids.slice(offset, offset + limit)
.map(id => this.db.object<Item>('/v0/item/' + id).valueChanges()))
) as Observable<Items>;
return merge(
combineLatest(this.queries, itemIds).pipe(
map(([query, ids]) => selector(query, ids).pipe(take(1)))
),
this.queries.pipe(
skip(1),
withLatestFrom(itemIds, selector)
)
).pipe(switchAll());
}
}
Listing 5-47. Load more stories
export class TopStoriesComponent implements OnInit, OnDestroy {
items: Items;
private subscription: Subscription;
private offset = 0;
private limit = 10;
constructor(private itemService: ItemService) { }
ngOnInit() {
this.subscription = this.itemService.get().subscribe(items => this.items = items);
this.doLoad(true);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
doLoad(refresh: boolean) {
this.itemService.load({
offset: this.offset,
limit: this.limit,
refresh,
});
this.offset += this.limit;
}
}
Pagination and Refresh
Pagination
Listing 5-48. Add pagination buttons
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Top Stories</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<div>
<ion-button color="light" [disabled]="!hasPrevious()" (click)="previous()">
<ion-icon name="arrow-back" slot="start"></ion-icon>
Prev
</ion-button>
<ion-button [disabled]="!canRefresh()" (click)="refresh()">
<ion-icon name="refresh" slot="icon-only"></ion-icon>
</ion-button>
<ion-button color="light" [disabled]="!hasNext()" (click)="next()">
<ion-icon name="arrow-forward" slot="end"></ion-icon>
Next
</ion-button>
</div>
<app-items [items]="items"></app-items>
</ion-content>
</ion-app>
Listing 5-49. Updated TopStories
export class TopStoriesComponent implements OnInit, OnDestroy {
items: Items;
private subscription: Subscription;
private offset = 0;
private limit = 10;
constructor(private itemService: ItemService) { }
ngOnInit() {
this.subscription = this.itemService.get().subscribe(items => this.items = items);
this.doLoad(true);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
hasPrevious(): boolean {
return this.offset > 0;
}
previous(): void {
if (!this.hasPrevious()) {
return;
}
this.offset -= this.limit;
this.doLoad(false);
}
hasNext(): boolean {
return this.items != null && (this.offset + this.limit) < this.items.total;
}
next() {
if (!this.hasNext()) {
return;
}
this.offset += this.limit;
this.doLoad(false);
}
canRefresh(): boolean {
return this.items != null;
}
refresh() {
if (!this.canRefresh()) {
return;
}
this.offset = 0;
this.doLoad(true);
}
private doLoad(refresh: boolean) {
this.itemService.load({
offset: this.offset,
limit: this.limit,
refresh,
});
}
}
Advanced List
Listing 5-50. Add ion-refresher and ion-infinite-scroll
<ion-content padding>
<ion-refresher slot="fixed" [disabled]="!canRefresh()" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<hnc-items [items]="items"></hnc-items>
<ion-infinite-scroll [disabled]="!hasNext()" (ionInfinite)="load($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
Listing 5-51. Updated TopStories
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import * as concat from 'lodash.concat';
import { Items } from '../../models/items';
import { ItemService } from '../../services/item/item.service';
@Component({
selector: 'app-top-stories',
templateUrl: './top-stories.component.html',
styleUrls: ['./top-stories.component.scss']
})
export class TopStoriesComponent implements OnInit, OnDestroy {
items: Items;
private subscription: Subscription;
private offset = 0;
private limit = 10;
private infiniteScrollComponent: any;
private refresherComponent: any;
constructor(private itemService: ItemService) { }
ngOnInit() {
this.subscription = this.itemService.get().subscribe(items => {
if (items.refresh) {
this.items = items;
this.notifyRefreshComplete();
} else {
this.items = {
...this.items,
results: concat(this.items.results, items.results),
};
this.notifyScrollComplete();
}
});
this.doLoad(true) ;
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
load(event) {
this.infiniteScrollComponent = event.target;
if (this.hasNext()) {
this.next();
}
}
hasNext(): boolean {
return this.items != null && (this.offset + this.limit) < this.items.total;
}
next() {
if (!this.hasNext()) {
return;
}
this.offset += this.limit;
this.doLoad(false);
}
canRefresh(): boolean {
return this.items != null;
}
refresh(event) {
this.refresherComponent = event.target;
if (this.canRefresh()) {
this.doRefresh();
}
}
doRefresh() {
this.offset = 0;
this.doLoad(true);
}
private doLoad(refresh: boolean) {
this.itemService.load({
offset: this.offset,
limit: this.limit,
refresh,
});
}
private notifyScrollComplete(): void {
if (this.infiniteScrollComponent) {
this.infiniteScrollComponent.complete();
}
}
private notifyRefreshComplete(): void {
if (this.refresherComponent) {
this.refresherComponent.complete();
}
}
}
Listing 5-52. Use waitFor to complete action
<ion-infinite-scroll (ionInfinite)="load($event.detail.
waitFor(doLoad()))">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
Customization
ion-infinite-scroll
Listing 5-53. ion-infinite-scroll customization
<ion-infinite-scroll-content
loadingSpinner="circles"
loadingText="Loading...">
</ion-infinite-scroll-content>
ion-refresher
Listing 5-54. ion-refresher customization
<ion-refresher-content
pullingIcon="arrow-dropdown"
pullingText="Pull to refresh"
refreshingSpinner="bubbles"
refreshingText="Loading...">
</ion-refresher-content>
Testing
Listing 5-55. Test for scrolling and refresh
let fixture: ComponentFixture<TopStoriesComponent> = null;
let component: any = null;
describe('top stories page', () => {
it('should show more items when scrolling down', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.next();
fixture.detectChanges();
fixture.whenStable().then(() => {
let debugElements = fixture.debugElement.queryAll(By.css('h2'));
expect(debugElements.length).toBe(20);
expect(debugElements[10].nativeElement.textContent).toContain('Item 11');
});
});
}));
it('should show first 10 items when refresh', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.next();
fixture.detectChanges();
fixture.whenStable().then(() => {
let debugElements = fixture.debugElement.queryAll(By.css('h2'));
expect(debugElements.length).toBe(20);
expect(debugElements[10].nativeElement.textContent).toContain('Item 11');
component.doRefresh();
fixture.detectChanges();
fixture.whenStable().then(() => {
let debugElements = fixture.debugElement.queryAll(By.css('h2'));
expect(debugElements.length).toBe(10);
expect(debugElements[0].nativeElement.textContent).toContain('Item 1');
});
});
});
}));
});