Создание приложения на Ionic с использованием API

  • Tutorial
image

Есть у меня приложение, написанное на Ionic Framework. На его основе хочу поделиться со всеми своим опытом разработки и напишу как создать кроссплатформенное приложение по шагам.

В этой статье будем с нуля разрабатывать приложение, которое позволяет читать статьи (публикации). У публикации будет название (заголовок), заглавное фото, краткое содержание, полное содержание, категория, автор, дата публикации. Все данные для приложения будут браться с сервера посредством Http-запросов.

В приложении будет несколько страниц (экранов):

  • список всех публикаций, отсортированный по дате.
  • список категорий, отсортированный по алфавиту.
  • список авторов, отсортированный по имени.
  • список публикаций выбранной категории, отсортированный по дате.
  • список публикаций выбранного автора, отсортированный по дате.
  • содержание публикации.

Результатом статьи получится приложение, которое выглядит как на картинке выше.
Плюс ссылка на исходники всего проекта.

Начало


Создадим новый проект и назовем его articles. Для этого выполним команду:

ionic start articles tabs

Результатом увидим созданный каталог с именем articles:

Структура проекта
image

Создадим новые страницы, которые нам нужны: postlist, categorylist, authorlist. Для этого поочередно выполним команды:

ionic generate page postlist
ionic generate page categorylist
ionic generate page authorlist


В результате увидим созданные каталоги в папке \articles\src\pages\:

Созданные страницы
image

Откроем файлы: postlist.ts, categorylist.ts, authorlist.ts и в каждом файле напишем строчку для импорта класса NavController и объекта NavParams

import { NavController, NavParams } from 'ionic-angular';

В результате получим следующий вид файлов postlist.ts, categorylist.ts, authorlist.ts

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-postlist',
  templateUrl: 'postlist.html',
})
export class Postlist {

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad Postlist');
  }

}

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-categorylist',
  templateUrl: 'categorylist.html',
})
export class Categorylist {

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad Categorylist');
  }

}

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-authorlist',
  templateUrl: 'authorlist.html',
})
export class Authorlist {

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad Authorlist');
  }

}

Удалим лишние папки в \articles\src\pages\: about, contact, home. Они были созданы автоматически при создании проекта. Нам они не нужны.

Откроем файл \src\app\app.module.ts и внесем туда изменения. Пропишем использование вновь созданных страниц и удалим всякие упоминания об удаленных страницах.

Результатом всех действий будет вот такое содержимое файла app.module.ts

app.module.ts
import { NgModule, ErrorHandler } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';

import { Postlist } from '../pages/postlist/postlist';
import { Categorylist } from '../pages/categorylist/categorylist';
import { Authorlist } from '../pages/authorlist/authorlist';
import { TabsPage } from '../pages/tabs/tabs';

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

@NgModule({
  declarations: [
    MyApp,
    Postlist,
    Categorylist,
    Authorlist,
    TabsPage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    Postlist,
    Categorylist,
    Authorlist,
    TabsPage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}


Откроем файл \src\pages\tabs\tabs.ts и изменим содержимое для того, чтобы вкладки ссылались на страницы postlist, categorylist, authorlist.

Измененный файл выглядит так:

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

import { Postlist } from '../postlist/postlist';
import { Categorylist } from '../categorylist/categorylist';
import { Authorlist } from '../authorlist/authorlist';

@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {

  tab1Root = Postlist;
  tab2Root = Categorylist;
  tab3Root = Authorlist;

  constructor() {

  }
}

Откроем файл \src\pages\tabs\tabs.html и внесем туда следующие изменения: изменим названия вкладок в tabTitle и иконок в tabIcon (все необходимые иконки перечислены в документации ionicons):

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="Публикации" tabIcon="ios-paper-outline"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="Категории" tabIcon="ios-albums-outline"></ion-tab>
  <ion-tab [root]="tab3Root" tabTitle="Авторы" tabIcon="ios-contacts-outline"></ion-tab>
</ion-tabs>

Выполним команду ionic serve, чтобы посмотреть полученный результат:

Первый результат
image

Поменяем основной цвет приложения на тот, который мы хотим. Для этого откроем файл \src\theme\variables.scss и добавим нужный цвет clmain:#3949ab в массиве $colors:

$colors: (
  primary:    #488aff,
  secondary:  #32db64,
  danger:     #f53d3d,
  light:      #f4f4f4,
  dark:       #222,
  clmain:     #3949ab,
);

И затем применим этот цвет в верхней части (для <ion-navbar>) каждой из страниц postlist.html, categorylist.html, authorlist.html:

<ion-navbar color="clmain">
  ...
</ion-navbar>

Переопределим цвет для вкладок (Tabs). Пропишем такие строки в файле \src\theme\variables.scss:

$tabs-md-tab-color-active: #283593;
$tabs-ios-tab-color-active: #283593;
$tabs-wp-tab-color-active: #283593;

В результате внешний вид приложения получится таким:

Основной цвет приложения
image

Меню


Теперь сделаем меню, которое выдвигается слева при нажатии на кнопку-гамбергер.

Создадим массив, в котором будут перечислены страницы с названиями, индексами и иконками. Откроем файл app.component.ts и сначала объявим массив pages в классе MyApp:

pages: Array<{title: string, component: any, index: string, icon_name: string}>;

а затем в конструкторе класса заполним этот массив:

this.pages = [
    { title: 'Публикации', component: TabsPage, index: '0', icon_name: 'ios-paper-outline' },
    { title: 'Категории', component: TabsPage, index: '1', icon_name: 'ios-albums-outline' },
    { title: 'Авторы', component: TabsPage, index: '2', icon_name: 'ios-contacts-outline' }
];

Также в самом начале импортируем используемые табы:

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

Используем заполненный массив pages для отображения пунктов меню. Откроем файл app.html и приведем его к следующему виду:

<ion-menu [content]="content">
  <ion-header>
	
  </ion-header>
  
  <ion-content>
    <ion-list no-lines>
      <button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)" color="clmain">
        <ion-icon item-left [name]="p.icon_name" item-left color="light"></ion-icon>
        {{p.title}}
      </button>
    </ion-list>
  </ion-content>

</ion-menu>

<ion-nav [root]="rootPage" #content></ion-nav>

Метод openPage(p) срабатывает при нажатии на пункт меню. В качестве параметра передается элемент массива нажатого пункта меню.

Опишем работу этого метода в файле app.component.ts

openPage(page) {
    this.navCtrl.setRoot(page.component, {index: page.index});
}

При вызове navCtrl.setRoot мы передаем страницу page.component, а также параметр page.index, являющийся индексом выбранного пункта. Этот параметр нужен нам будет, чтобы знать какая из трех вкладок открывается.

navCtrl — объявляется следующим образом (всё в том же файле app.component.ts):

import { ViewChild } from '@angular/core';
import { Nav } from 'ionic-angular';

и в классе MyApp в самом начале пишем объявление:

@ViewChild(Nav) navCtrl: Nav;

В результате получаем следующее содержимое файла app.component.ts:

app.component.ts
import { Component, ViewChild } from '@angular/core';
import { Nav, Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';

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

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  @ViewChild(Nav) navCtrl: Nav;
  rootPage:any = TabsPage;
  
  pages: Array<{title: string, component: any, index: string, icon_name: string}>;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      statusBar.styleDefault();
      splashScreen.hide();
    });

    this.pages = [
      { title: 'Публикации', component: TabsPage, index: '0', icon_name: 'ios-paper-outline' },
      { title: 'Категории', component: TabsPage, index: '1', icon_name: 'ios-albums-outline' },
      { title: 'Авторы', component: TabsPage, index: '2', icon_name: 'ios-contacts-outline' }
    ];
	
  }

  openPage(page) {
    this.navCtrl.setRoot(page.component, {index: page.index});
  }
  
}


Теперь сделаем выделение именно той вкладки, которая должна быть открыта при нажатии определенного пункта меню: «Публикации», «Категории», «Авторы». Для этого откроем файл tabs.ts и напишем получение параметра Index (который передается в методе openPage(page)).

Импортируем NavParams:

import { NavParams } from 'ionic-angular';

Объявим новую переменную index в классе TabsPage:

index: string;

В параметрах конструктора впишем:

public navParams: NavParams

а в теле конструктора напишем получение значения индекса:

this.index = navParams.get('index');

Полностью файл tabs.ts будет выглядеть так:

import { Component } from '@angular/core';
import { NavParams } from 'ionic-angular';

import { Postlist } from '../postlist/postlist';
import { Categorylist } from '../categorylist/categorylist';
import { Authorlist } from '../authorlist/authorlist';

@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {

  index: string;
  
  tab1Root = Postlist;
  tab2Root = Categorylist;
  tab3Root = Authorlist;

  constructor(public navParams: NavParams) {
    this.index = navParams.get('index');

  }
}

И еще добавим выделение нужной вкладки по полученному индексу. Для этого в файле tabs.html для <ion-tabs> напишем следующее:

<ion-tabs selectedIndex={{index}}>

Сделаем для всех меню фон таким же как наш основной цвет. Для этого откроем файл \src\app\app.scss и добавим туда стиль:

.myBg{
	background-color: #3949ab;
}

Откроем файл app.html и применим этот стиль для элемента <ion-content>:

<ion-content class="myBg">


Левое меню
image

Добавим следующие строки (внутри элемента <ion-navbar>) в файлы postlist.html, categorylist.html, authorlist.html, чтобы увидеть иконку меню (гамбургер) в левой части верхней строки приложения.

<button ion-button menuToggle>
    <ion-icon name="menu"></ion-icon>
</button>

Посмотрим результат и увидим такой вид:

Иконка меню
image

Ну и в качестве эксперимента добавим рисунок перед выводом всех пунктом меню. Для этого возьмем картинку любого размера и поместим ее в папку \src\assets\imgs\ с именем menu.png. Затем будем ее использовать в файле app.html:

<ion-content class="myBg">
   <img src="assets/imgs/menu.png" />
   <ion-list no-lines>
      <button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)" color="clmain">
         <ion-icon item-left [name]="p.icon_name" item-left color="light"></ion-icon>
         {{p.title}}
      </button>
   </ion-list>
</ion-content>

Посмотрим результат и увидим следующее:

Меню с рисунком
image

В результате проделанной работы мы получили приложение, которое содержит:

  • три страницы: «Публикации», «Категории», «Авторы».
  • меню, выдвигающееся слева.
  • табы для переключения страниц.
  • пункты меню для переключения табов.

Получение данных


Дальше будем заполнять каждую из страниц (postlist, categorylist, authorlist) данными, которые будем получает в формате JSON посредством HTTP запросов.

Вот список пунктов, которые будут реализовываны для каждой страницы:

  • загрузка данных при открытии страницы.
  • подгрузка следующих данных при пролистывании к последнему элементу списка.
  • обновление страницы при потягивании списка вниз, когда список отображает самое начало.
  • отображение спиннера загрузки (в момент выполнения запроса при получении данных).
  • обработка ошибок при выполнении запроса.

Пару слов о выполнении запросов в Ionic. При работе с запросами нужно помнить о технологии CORS.
В моем примере я использую свой сервер для получения данных, соответственно при получении данных с сервера в заголовках я без проблем указываю нужный заголовок:

header('Content-Type: application/json;charset=utf-8');
header('Access-Control-Allow-Origin: *');

Например полный текст скрипта для получения списка категорий выглядит примерно так:

Скрипт для получения списка категорий
header('Content-Type: application/json;charset=utf-8');
header('Access-Control-Allow-Origin: *');

$dblocation = "localhost";
$dbname = "database";
$dbuser = "username";
$dbpasswd = "password";

$mysqli = new mysqli($dblocation, $dbuser, $dbpasswd, $dbname);

$query = "
	select `tb_categories`.*
	from `tb_categories`
	order by `tb_categories`.`category`";
	
$data = array();

if ($res = $mysqli->query($query))
{
	$data['count'] = strval($res->num_rows);

	while($row = $res->fetch_assoc()){
	 
		$data['data'][] = array(
			'id' => $row['id'],
			'category' => $row['category'],
			'url' => $row['url']
		);

	}
	
}
	
echo json_encode($data);


При выполнении этого скрипта мы получим в качестве ответа данные в формате JSON:

JSON-данные
image

Возвращаемся к Ionic-приложению. Начнем с подключения сервиса Http. Сначала в файле app.module.ts импортируем HttpModule следующей строкой:

import { HttpModule } from '@angular/http';

а также пропишем его в разделе импорта:

imports: [
    BrowserModule,
    HttpModule,
    IonicModule.forRoot(MyApp)
],

Далее переходим к файлу postlist.ts и описываем импорт следующих объектов:

import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';

LoadingController используется для отображения индикатора загрузки.

объявляем в конструкторе класса Postlist сервис Http и LoadingController:

constructor(public navCtrl: NavController, public http: Http, public loadingCtrl: LoadingController, public navParams: NavParams)

Дальше я приведу полное содержимое файла postlist.ts с комментариями:

postlist.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';

@Component({
  selector: 'page-postlist',
  templateUrl: 'postlist.html',
})
export class Postlist {

  postlists: any;            // данные со списком публикаций, полученные из запроса
  postlists_new: any;        // данные СЛЕДУЮЩЕГО списка публикаций, которые получаются при пролистывании списка к последнему элементу
  countElement: number = 10; // кол-во элементов, которые мы получаем из запроса
  beginElement: number = 0;  // начальный номер публикации, с которого получаем список элементов
  post_error: string;        // результат выполнения запроса 0-успешно, 1-ошибка
  
  constructor(public navCtrl: NavController, public http: Http, public loadingCtrl: LoadingController, public navParams: NavParams) {
	// Метод получения данных из запроса
	// 0 - получаем данные с самого начала
	// 1 - получаем СЛЕДУЮЩИЕ данные по порядку 
	this.loadData(0);
  }

  loadData(isNew) {
    if (isNew==0){
	  // Первоначальные значения переменных
	  this.beginElement = 0;
	  this.countElement = 10;
  
      // Создаем окно загрузки
      let loadingPopup = this.loadingCtrl.create({
        content: ''
      });

      // Показываем окно загрузки
      loadingPopup.present();	
	
      // Получение данных, с указание URL-запроса и параметров
      this.http.get('https://mysite.ru/postlist.php?begin='+this.beginElement+'&limit='+this.countElement)
	    .timeout(20000)   // Ставим лимит на получение запроса и прерываем запрос через 20 сек.
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.postlists = data.data;     // Данные получены, записываем их
              this.countElement = data.count; // Записываем кол-во полученных публикаций
  			  this.post_error = "0";          // Результат - успешно
              loadingPopup.dismiss();         // Убираем окно загрузки
            }, 1000);

          },
          err => {
			loadingPopup.dismiss();           // Убираем окно загрузки
			this.post_error = "1";            // Результат - ошибка
		  }
      );	
	
	}else{
	  // Увеличиваем начальную позицию номера публикации для последующего получения именно с нужной позиции
	  this.beginElement = Number(this.beginElement) + Number(this.countElement);
	}
  }
  
  // Выполняется при пролистывании к последнему элементу списка
  doInfinite(infiniteScroll) {

	// Проверяем нужно ли выполнять запрос
	// Если в предыдущем запросе мы получили 0 публикаций,
	//   значит больше не нужно выполнять запрос для получения СЛЕДУЮЩЕГО набора данных
	if (this.countElement != 0){
      this.loadData(1);

      // Get the data
      this.http.get('https://mysite.ru/postlist.php?begin='+this.beginElement+'&limit='+this.countElement)
	    .timeout(20000)
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.postlists_new = data.data;  // Записали новую порцию данных
              this.countElement = data.count;
  			  this.post_error = "0";

			  for (let i = 0; i < this.countElement; i++) {
    			this.postlists.push( this.postlists_new[i] );  // Добавили новые данные в основной массив публикаций
			  }
			  
		      infiniteScroll.complete();
            }, 1000);

          },
          err => console.error(err)
      );
	  
	}else{
	  infiniteScroll.complete();
	}
  }
  
  // Выполняется при потягивании списка вниз, когда список находится в верхнем положении
  doRefresh(refresher) {
    
	this.loadData(0);
    
	setTimeout(() => {
      refresher.complete();
    }, 2000);
  }
    
  ionViewDidLoad() {
    console.log('ionViewDidLoad Postlist');
  }

}


Откроем файл postlist.html и сделаем отображение полученных данных в виде списка:

...
<ion-content padding>

  <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="arrow-dropdown"
      pullingText="Потяните для обновления"
      refreshingSpinner="circles"
      refreshingText="Обновление...">
    </ion-refresher-content>
  </ion-refresher>

  <div *ngIf="post_error == '0'">
  <ion-card *ngFor="let postlist of postlists" (click)="openPostPage(postlist)" text-wrap class="list-selected">
    <ion-card-title class="postlist-title"></ion-card-title>
	  <div>
	    <div class="postlist-category">{{postlist.category}}</div>
	    <div class="postlist-dat">{{postlist.dat3}}</div>
	  </div>
    <ion-card-title class="postlist-title">
      {{postlist.title}}
    </ion-card-title>
    <img [src]="postlist.img" />
	<h5 class="postlist-intro-text">{{postlist.intro_text}}</h5>
  </ion-card>
  </div>
  <div *ngIf="post_error == '1'" style="text-align: center">
    <ion-label>Ошибка при получении данных</ion-label>
    <button ion-button (click)="loadData(0)" color="clmain" icon-left>
	  <ion-icon name="refresh"></ion-icon>
	  Обновить
    </button>
  </div>
  
  <ion-infinite-scroll (ionInfinite)="doInfinite($event)">
    <ion-infinite-scroll-content
      loadingSpinner="bubbles"
      loadingText="Загрузка данных...">	
	</ion-infinite-scroll-content>
  </ion-infinite-scroll>
  
</ion-content>

Теперь пару слов об этом коде. В верхней части в блоке <ion-refresher> описаны действия, при потягивании списка вниз, когда список отображает самое начало. В нижней части в блоке <ion-infinite-scroll> описаны действия, при пролистывании к последнему элементу списка.

В центральной части два блока div. Один отображается при условии, что нет ошибки при получении данных (post_error == '0'). Второй отображается, если была ошибка (post_error == '1').

Результат получается таким
image

Теперь немного приукрасим отображение. Для этого опишем необходимые стили (postlist-title, postlist-intro-text, postlist-dat, postlist-category) в файле postlist.scss:

page-postlist {

	.postlist-title {
		font-size: 18px !important;
		white-space: inherit;
	}

	.postlist-intro-text {
		font-size: 14px !important;
		color: gray;
		white-space: inherit;
	}

	.postlist-dat {
		font-size: 12px !important;
		color: gray;
		white-space: inherit;
		float: right;
		text-align: right;
		width: 50%;
	}

	.postlist-category {
		font-size: 12px !important;
		color: gray;
		white-space: inherit;
		float: left;
		width: 50%;
	}

}

Результат получится таким
image

Загрузка данных
image

Ошибка получения данных
image

Обновление данных
image

Подгрузка следующих данных
image

При нажатии на элемент списка (<ion-card>) срабатывает событие click. И у нас там написан вызов метода openPostPage(postlist). Данный метод позволит открыть содержимое публикации. Позднее мы вернемся к нему и опишем его.

Проводим аналогичные действия для оставшихся двух страниц
В categorylist будем отображать список всех категорий публикаций.
В authorlist будем отображать список всех пользователей публикаций.
Чуть ниже приведу сразу готовые файлы каждой страницы, потому что методика получения и отображения данных в них такая же как и для страницы postlist.

Единственное исключение касается категорий. Т.к. категорий будет малое кол-во в принципе, то для этой страницы не нужно делать подгрузку следующих данных при достижении конца списка. Сразу получим все категории и отобразим их целиком.

Дополнительно сделаем еще один функционал: при нажатии на элемент списка (либо на категорию либо на автора) откроем список публикаций для выбранного элемента. Для этого у события click напишем вызов методов openPostCategoryPage и openPostAuthorPage соответственно в каждой странице, а также опишем работу методов (в обоих файлах categorylist.ts и authorlist.ts):

openPostCategoryPage(item) {
    this.navCtrl.push(Postlist, { item: item, type: '1' });	
}

openPostAuthorPage(item) {
    this.navCtrl.push(Postlist, { item: item, type: '2' });	
}

В качесте параметра передадим выбранную страницу (item) и номер страницы (type), чтобы потом отличить страницу категорий от страницы авторов.

Вот полное содержимое файлов categorylist.ts, categorylist.html и authorlist.ts, authorlist.html.

categorylist.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';
import { Postlist } from '../postlist/postlist';

@Component({
  selector: 'page-categorylist',
  templateUrl: 'categorylist.html',
})
export class Categorylist {

  categorylists: any;
  post_error: string;
  
  constructor(public navCtrl: NavController, public navParams: NavParams, public http: Http, public loadingCtrl: LoadingController) {
    this.loadData();
  }

  openPostCategoryPage(item) {
    this.navCtrl.push(Postlist, { item: item, type: '1' });	
  }
  
  loadData() {
      // Создаем окно загрузки
      let loadingPopup = this.loadingCtrl.create({
        content: ''
      });

      // Показываем окно загрузки
      loadingPopup.present();	
	
      // Получение данных, с указание URL-запроса
      this.http.get('https://mysite.ru//categorylist.php')
	    .timeout(20000)
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.categorylists = data.data;
  			  this.post_error = "0";
              loadingPopup.dismiss();
            }, 1000);

          },
          err => {
			loadingPopup.dismiss();
			this.post_error = "1";
		  }
      );	
		  
  }
  
  // Выполняется при потягивании списка вниз, когда список находится в верхнем положении
  doRefresh(refresher) {
    
	this.loadData();
    
	setTimeout(() => {
      refresher.complete();
    }, 2000);
  }
  
  ionViewDidLoad() {
    console.log('ionViewDidLoad Categorylist');
  }

}


categorylist.html
<ion-header>
  <ion-navbar color="clmain">
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Категории</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="arrow-dropdown"
      pullingText="Потяните для обновления"
      refreshingSpinner="circles"
      refreshingText="Обновление...">
    </ion-refresher-content>
  </ion-refresher>
  
  <div *ngIf="post_error == '0'">
  <ion-list>
    <button ion-item *ngFor="let categorylist of categorylists" (click)="openPostCategoryPage(categorylist)" text-wrap class="list-selected">
      <ion-avatar item-left>
        <img [src]="categorylist.icon" />
      </ion-avatar>
	  <ion-label class="categorylist-title">{{categorylist.category}}</ion-label>
    </button>
  </ion-list>
  </div>
  <div *ngIf="post_error == '1'" style="text-align: center">
    <ion-label>Ошибка при получении данных</ion-label>
    <button ion-button (click)="loadData(0)" color="clmain" icon-left>
	  <ion-icon name="refresh"></ion-icon>
	  Обновить
    </button>
  </div>
  
</ion-content>


authorlist.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';
import { Postlist } from '../postlist/postlist';

@Component({
  selector: 'page-authorlist',
  templateUrl: 'authorlist.html',
})
export class Authorlist {

  authorlists: any;
  authorlists_new: any;
  countElement: number = 40;
  beginElement: number = 0;
  post_error: string;
  
  constructor(public navCtrl: NavController, public navParams: NavParams, public http: Http, public loadingCtrl: LoadingController) {
	this.loadData(0);
  }

  openPostAuthorPage(item) {
    this.navCtrl.push(Postlist, { item: item, type: '2' });	
  }
  
  loadData(isNew) {
    if (isNew==0){
	  // Первоначальные значения переменных
	  this.beginElement = 0;
	  this.countElement = 40;

      // Создаем окно загрузки
      let loadingPopup = this.loadingCtrl.create({
        content: ''
      });

      // Показываем окно загрузки
      loadingPopup.present();	
	
      // Получение данных, с указание URL-запроса и параметров
      this.http.get('https://mysite.ru/authorlist.php?begin='+this.beginElement+'&limit='+this.countElement)
	    .timeout(20000)
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.authorlists = data.data;
              this.countElement = data.count;
  			  this.post_error = "0";
              loadingPopup.dismiss();
            }, 1000);

          },
          err => {
			loadingPopup.dismiss();
			this.post_error = "1";
		  }
      );	
	  
	}else{
	  // Увеличиваем начальную позицию номера публикации для последующего получения именно с нужной позиции
	  this.beginElement = Number(this.beginElement) + Number(this.countElement);
	
	}
  }
  
  // Выполняется при пролистывании к последнему элементу списка
  doInfinite(infiniteScroll) {

	// Проверяем нужно ли выполнять запрос
	// Если в предыдущем запросе мы получили 0 публикаций,
	//   значит больше не нужно выполнять запрос для получения СЛЕДУЮЩЕГО набора данных
	if (this.countElement != 0){
      this.loadData(1);

      // Получение данных, с указание URL-запроса и параметров
      this.http.get('https://mysite.ru/authorlist.php?begin='+this.beginElement+'&limit='+this.countElement+'&t='+this.searchtext)
	    .timeout(20000)
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.authorlists_new = data.data;
              this.countElement = data.count;
  			  this.post_error = "0";
			  
			  for (let i = 0; i < this.countElement; i++) {
			    this.authorlists.push( this.authorlists_new[i] );
			  }

			  infiniteScroll.complete();
            }, 1000);

          },
          err => console.error(err)
      );	
	
	}else{
	  infiniteScroll.complete();
	}
  }
  
  // Выполняется при потягивании списка вниз, когда список находится в верхнем положении
  doRefresh(refresher) {
    
	this.loadData(0);
    
	setTimeout(() => {
      refresher.complete();
    }, 2000);
  }
  
  ionViewDidLoad() {
    console.log('ionViewDidLoad Authorlist');
  }

}


authorlist.html
<ion-header>
  <ion-navbar color="clmain">
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Авторы</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding class="android-scroll-bar">
  <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="arrow-dropdown"
      pullingText="Потяните для обновления"
      refreshingSpinner="circles"
      refreshingText="Обновление...">
    </ion-refresher-content>
  </ion-refresher>

  <div *ngIf="post_error == '0'">
  <ion-list>
    <button ion-item *ngFor="let authorlist of authorlists" (click)="openPostAuthorPage(authorlist)" text-wrap class="list-selected">
      <ion-avatar item-left>
        <img [src]="authorlist.img" />
      </ion-avatar>
	  <ion-label class="authorlist-title">{{authorlist.author}}</ion-label>
    </button>
  </ion-list>
  </div>
  <div *ngIf="post_error == '1'" style="text-align: center">
    <ion-label>Ошибка при получении данных</ion-label>
    <button ion-button (click)="loadData(0)" color="clmain" icon-left>
	  <ion-icon name="refresh"></ion-icon>
	  Обновить
    </button>
  </div>
  
  <ion-infinite-scroll (ionInfinite)="doInfinite($event)">
    <ion-infinite-scroll-content
      loadingSpinner="bubbles"
      loadingText="Загрузка данных...">	
	</ion-infinite-scroll-content>
  </ion-infinite-scroll>
 
</ion-content>


Результат отображения категорий и авторов
imageimage

Чтобы отобразить список публикаций выбранной категории и выбранного автора, будем использовать уже существующую страницу postlist.

Для этого в Http запросах введем параметр C для передачи значения выбранной категории и параметр A для передачи выбранного автора. Если данный параметр не заполнен, будем возвращать все публикации.

После внесения изменений в файлы postlist.ts и postlist.html получим следущее:

postlist.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';

@Component({
  selector: 'page-postlist',
  templateUrl: 'postlist.html',
})
export class Postlist {

  title: string;
  categoryId: any;
  authorId: any;
  selectedItem: any;
  selectedType: string;
  postlists: any;            // данные со списком публикаций, полученные из запроса
  postlists_new: any;        // данные СЛЕДУЮЩЕГО списка публикаций, которые получаются при пролистывании списка к последнему элементу
  countElement: number = 10; // кол-во элементов, которые мы получаем из запроса
  beginElement: number = 0;  // начальный номер публикации, с которого получаем список элементов
  post_error: string;        // результат выполнения запроса 0-успешно, 1-ошибка
  
  constructor(public navCtrl: NavController, public http: Http, public loadingCtrl: LoadingController, public navParams: NavParams) {
	this.selectedItem = navParams.get('item');	
	this.selectedType = navParams.get('type');	
	
	this.categoryId = '';
	this.authorId = '';
	
	this.title = 'Публикации';
	if (this.selectedType == '1'){
		this.title = this.selectedItem.category;
		this.categoryId = this.selectedItem.id;
	}
	
	if (this.selectedType == '2'){
		this.title = this.selectedItem.author;
		this.authorId = this.selectedItem.id;
	}
	
	// Метод получения данных из запроса
	// 0 - получаем данные с самого начала
	// 1 - получаем СЛЕДУЮЩИЕ данные по порядку
	this.loadData(0);
  }

  loadData(isNew) {
    if (isNew==0){
	  // Первоначальные значения переменных
	  this.beginElement = 0;
	  this.countElement = 10;
  
      // Создаем окно загрузки
      let loadingPopup = this.loadingCtrl.create({
        content: ''
      });

      // Показываем окно загрузки
      loadingPopup.present();	
	
      // Получение данных, с указание URL-запроса и параметров
      this.http.get('https://mysite.ru/postlist.php?begin='+this.beginElement+'&limit='+this.countElement+'&c='+this.categoryId+'&a='+this.authorId)
	    .timeout(20000)   // Ставим лимит на получение запроса и прерываем запрос через 20 сек.
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.postlists = data.data;     // Данные получены, записываем их
              this.countElement = data.count; // Записываем кол-во полученных публикаций
  			  this.post_error = "0";          // Результат - успешно
              loadingPopup.dismiss();         // Убираем окно загрузки
            }, 1000);

          },
          err => {
			loadingPopup.dismiss();           // Убираем окно загрузки
			this.post_error = "1";            // Результат - ошибка
		  }
      );	
	
	}else{
	  // Увеличиваем начальную позицию номера публикации для последующего получения именно с нужной позиции
	  this.beginElement = Number(this.beginElement) + Number(this.countElement);
	}
  }
  
  // Выполняется при пролистывании к последнему элементу списка
  doInfinite(infiniteScroll) {

	// Проверяем нужно ли выполнять запрос
	// Если в предыдущем запросе мы получили 0 публикаций,
	//   значит больше не нужно выполнять запрос для получения СЛЕДУЮЩЕГО набора данных
	if (this.countElement != 0){
      this.loadData(1);

      // Получение данных, с указание URL-запроса и параметров
      this.http.get('https://mysite.ru/postlist.php?begin='+this.beginElement+'&limit='+this.countElement+'&c='+this.categoryId+'&a='+this.authorId)
	    .timeout(20000)
        .map(res => res.json())
        .subscribe(
          data => {

            setTimeout(() => {
              this.postlists_new = data.data;  // Записали новую порцию данных
              this.countElement = data.count;
  			  this.post_error = "0";

			  for (let i = 0; i < this.countElement; i++) {
    			this.postlists.push( this.postlists_new[i] );  // Добавили новые данные в основной массив публикаций
			  }
			  
		      infiniteScroll.complete();
            }, 1000);

          },
          err => console.error(err)
      );
	  
	}else{
	  infiniteScroll.complete();
	}
  }
  
  // Выполняется при потягивании списка вниз, когда список находится в верхнем положении
  doRefresh(refresher) {
    
	this.loadData(0);
    
	setTimeout(() => {
      refresher.complete();
    }, 2000);
  }
    
  ionViewDidLoad() {
    console.log('ionViewDidLoad Postlist');
  }

}


В файле postlist.html изменения коснутся только в части отображения заголовка:

<ion-header>
  <ion-navbar color="clmain">
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>{{title}}</ion-title>
  </ion-navbar>
</ion-header>

В результате всех изменений теперь можно просматривать публикации выбранной категории и выбранного автора:

Публикации выбранной категории и выбранного автора
imageimage

Осталось сделать последнюю страницу для отображения содержимого публикации. А именно: заголовок, дата, категория, автор, фото, краткое содержание, полное содержание.

Для этого создадим новую страницу командой:

ionic generate page post

Внесем изменения в файл app.module.ts. Добавим строку для импорта:

import { Post } from '../pages/post/post';

а также пропишем созданную страницу в секциях declarations и entryComponents.

  ...
  declarations: [
    MyApp,
    Postlist,
    Categorylist,
    Authorlist,
    TabsPage,
    Post
  ],
  ...
  entryComponents: [
    MyApp,
    Postlist,
    Categorylist,
    Authorlist,
    TabsPage,
    Post
  ],
  ...

В файле post.ts напишем строчку для импорта класса NavController и объекта NavParams

import { NavController, NavParams } from 'ionic-angular';

Далее методика аналогичная: получаем данные публикации посредством запроса и отображаем их в нужном виде. Результат готовых измененных файлов:

post.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/timeout';
import { LoadingController } from 'ionic-angular';

Component({
selector: 'page-post',
templateUrl: 'post.html',
})
export class Post {

selectedItem: any;
postphotos: any;
post_category: any;
post_author: any;
post_author_id: any;
post_author_img: any;
post_title: any;
post_dat3: any;
post_intro_text: any;
post_full_text: any;
post_img: any;
post_is_photo: any;
post_error: string;

constructor(public navCtrl: NavController, public http: Http, public loadingCtrl: LoadingController, public navParams: NavParams) {
this.selectedItem = navParams.get('item');

this.loadData();
}

loadData() {
// Создаем окно загрузки
let loadingPopup = this.loadingCtrl.create({
content: ''
});

// Показываем окно загрузки
loadingPopup.present();

// Получение данных, с указание URL-запроса и параметров
this.http.get('https://mysite.ru/post.php?p='+this.selectedItem.id)
.timeout(20000)
.map(res => res.json())
.subscribe(
data => {

setTimeout(() => {
this.postphotos = data.data;
this.post_category = data.category;
this.post_author = data.author
this.post_author_id = data.author_id;
this.post_author_img = data.author_img;
this.post_title = data.title;
this.post_dat3 = data.dat3;
this.post_intro_text = data.intro_text;
this.post_full_text = data.full_text;
this.post_img = data.img;
this.post_is_photo = data.is_photo;
this.post_error = «0»;
loadingPopup.dismiss();
}, 1000);

},
err => {
loadingPopup.dismiss();
this.post_error = «1»;
}
);

}

// Выполняется при потягивании списка вниз, когда список находится в верхнем положении
doRefresh(refresher) {

this.loadData();

setTimeout(() => {
refresher.complete();
}, 2000);
}

ionViewDidLoad() {
console.log('ionViewDidLoad Post');
}

}

post.html
<ion-header>
  <ion-navbar color="clmain">
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Содержание</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content
      pullingIcon="arrow-dropdown"
      pullingText="Потяните для обновления"
      refreshingSpinner="circles"
      refreshingText="Обновление...">
    </ion-refresher-content>
  </ion-refresher>
  
  <div *ngIf="post_error == '0'">
    <h3></h3>
	  <div>
	    <div class="post-category">{{post_category}}</div>
	    <div class="post-dat">{{post_dat3}}</div>
	  </div>
	  <h3 class="post-title">{{post_title}}</h3>
      <img *ngIf="post_is_photo != '1'" src="{{post_img}}" />
	  <h5 class="post-intro-text">{{post_intro_text}}</h5>
	  <div class="post-text" [innerHTML] = "post_full_text"></div>
  
    <button ion-item>
      <ion-avatar item-left>
        <img [src]="post_author_img" />
      </ion-avatar>
	  <ion-label class="author-title">{{post_author}}</ion-label>
	</button>
  </div>
  <div *ngIf="post_error == '1'" style="text-align: center">
    <ion-label>Ошибка при получении данных</ion-label>
    <button ion-button (click)="loadData(0)" color="clmain" icon-left>
	  <ion-icon name="refresh"></ion-icon>
	  Обновить
    </button>
  </div>

</ion-content>


post.scss
page-post {

	.post-title {
		font-size: 19px !important;
		white-space: inherit;
	}

	.post-intro-text {
		font-size: 15px !important;
		color: gray;
		white-space: inherit;
	}

	.post-dat {
		font-size: 14px !important;
		color: gray;
		white-space: inherit;
		float: right;
		text-align: right;
		width: 50%;
	}

	.post-category {
		font-size: 14px !important;
		color: gray;
		white-space: inherit;
		float: left;
		width: 50%;
	}

	.post-text {
		font-size: 16px !important;
	}

}


Теперь вспомним про метод openPostPage(), который вызывается в postlist.html у события click.

Этот метод позволит открыть страницу с содержимым публикации. Описываем метод в postlist.ts:

openPostPage(item) {
    this.navCtrl.push(Post, { item: item });	
}  

а также импортируем страницу Post:

import { Post } from '../post/post';

Проверяем результат всех внесенных изменений и видим страницу, которая открывается при нажатии на публикацию в списке.

Содержание страницы
image

Вот пожалуй и весь основной функционал, который позволит листать публикации и читать их содержимое, данные для которых берутся с сайта посредством Http запросов.

Исходники данного примера-проекта можно посмотреть на GitHub
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 9
  • +2
    Может Вам стоит как то более логично закончить статью? Ну, там выделить какие то отличии, подчеркнуть в статье вещи, которые Вы вынесли в заголовок, ну, если они действительно как-то особо рассматриваются в Вашей статье (API, ionic). Километры исходников можно было не выкладывать — у Вас есть ссылка на гит хаб, кто в «теме» поймет без комментариев, кто не очень — пройдет мимо.
    Я не хочу негативно высказываться, но «о чем Ваша статья»? Что я должен вынести для себя после прочтения ее? Что появился какой то очередной кросс-велосипед для моб. сегмента. Так этим никого не удивишь. Ни метрик, ни сравнений (хотя бы с аналогичным нативным).
    Печально в общем…
    • +1
      В том то и дело, что статья НЕ о сравнении чего-то. Поэтому подводить итоги и делать сравнения не с чем. Я описал путь разработки приложения от начала и до конца. Чтобы новичок, который начал писать приложение на Ionic смог бы самостоятельно это сделать. А также смог бы понять, что и откуда берется.
      Во-первых, километры исходников спрятаны в спойлеры. Во-вторых, каждый шаг расписан и показывает, что изменилось и что получилось.
      Я считаю, что нельзя кидать готовый код без объяснения, что, как и почему работает. Я же в статье хочу показать, как можно сделать приложение и что для этого нужно написать.
      Еще раз подчеркну. Это Tutorial по созданию приложения по шагам — а не вариант сравнения между нативным и кроссплатформенным.
      • 0

        за туториал конечно спасибо, но если Вы хотели показать все шаги для новичков, то где билд под android ios

        • 0
          Я считаю, что окончательный build по различные платформы может быть вполне отдельной статьей.
          Дело в том, что недостаточно просто взять и выполнить команду build.
          Нужно настроить проект в config.xml и др., настроить resources, такие как Icons, SplashScreen и т.д.
          В данной статье я тестировал и показывал результаты, которые отображаются в браузере при выполнении команды ionic serve
    • 0
      А почему вы не стали использовать ленивую загрузку страниц, причем она идет сейчас по-умолчанию? Тогда вместо
      this.navCtrl.push(Postlist, { item: item, type: '1' });
      будет
      this.navCtrl.push('Postlist', { item: item, type: '1' });
      и импортировать ничего никуда тогда не надо.
      • 0
        Потому что о такой возможности я даже и не знал.
        Вот вы впервые сказали мне.
        • 0
          Для этого в папке с каждой странице должен лежать файл с именем что-то типа home.module.ts
          И кстати, если использовать генератор страниц от ionic, то в последних его версиях он создает страницы уже с этим файлом. Так что в файле app.module.ts нужно подключать общие нативные модули, что сильно уменьшает и его размер и читаемость кода ))
        • +1
          Небольшой ремарк.
          HttpModule находится в статусе deprecated и будет выкошен в следующих версиях angular (не знаю в каких)
          Вместо HttpModule необходимо искользовать HttpClientModule.
          Так же можете глянуть в сторону библиотчки @ngx-resource/core в связке с @ngx-resource/handler-ngx-http

          По поводу CORS в приложении (на девайсе, не браузере) можно использовать нативный модуль HTTP.

          В своих приложения я использую оба метода http запросов:
          • обычный с @ngx-resource/handler-ngx-http, когда приложение запущено из браузера и запросы к API происходят в том же домене (нет проблем c CORS)
          • нативный с HTTP обернутый в @ngx-resource/handler-cordova-advanced-http,
            когда приложение «нативное» (нет проблем c CORS)
          • +1
            Что можно доработать:
            1. Ленивая загрузка компонентов (как выше уже написали). Примеры:

            2. Адаптивный вывод карточек.
            Например, пользователь будет просматривать приложение на планшете да ещё в альбомном режиме. Как будут отображаться карточки с информацией в данном случае? Они растянутся на весь экран. Можно организовать вывод количества карточек в строке в зависимости от ширины экрана. Пример:

            3. Перенести загрузку и работу с данными в выделенный сервис/сервисы, вместо того, чтобы работать с данными напрямую в компонентах. Например, сервис Репозиторий — для работы с данными (сортировка, фильтрация и т.п.) и сервис Источник данных — для загрузки данных. Компоненты запрашивают данные и их необходимую обработку у сервиса Репозитория, который в свою очередь получает данные из источника. Источники данных можно в дальнейшем, при необходимости, менять. При этом при смене источника данных как-то менять классы компонентов и класс сервиса Репозитория не потребуется. Потребуется всего лишь в модуле сервиса Репозитория сменить класс сервиса Источника данных. Т.е. будет что-то в таком виде:
            @NgModule({
                //...
                providers: [
                    //...
                    { provide: dataSource, useClass: prodDataSource}
                ],
                //...
            })
            

            Тут оба класса dataSource и prodDataSource должны определять одинаковые методы. При этом класс dataSource определяет просто интерфейс, т.е. набор пустых методов или методов, возвращающие пустой массив данных. В свою очередь каждый класс сервиса реального источника данных (например, prodDataSource) эти методы по-своему реализует.
            4. Добавить классы-модели данных.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое