В прошлом уроке мы с вами познакомились с бэкэндом на Yii2, создав REST api. Ссылка на предыдущий урок. Теперь пришло время создать фронтенд, который будет работать с нашими данными. Для этой цели мы будем использовать Angular2. Это фреймворк, функция которого - делать прикольные SPA. Поехали!
Я развернул ангуляр в корне сайта, задав команду: (Обязательно учтите, что должен быть установлен пакетный менеджер NPM, скачать его можно с сайта: https://nodejs.org/en/download/)
ng new angular
Там внутри папки, в файле: .angular-cli.json я изменил папку для вывода:
"apps": [
{
...
"outDir": "../web/angular/dist",
...
}
Мой проект собирается в папке web (сюда смотрит DOCUMENT_ROOT)
Чтобы у нас открывались скрипты, добавим их в файл asset/AppAsset.php
public $js = [
'angular/dist/inline.bundle.js',
'angular/dist/polyfills.bundle.js',
'angular/dist/vendor.bundle.js',
'angular/dist/main.bundle.js',
];
Полный листинг функции смотрите на гите. Этот код подключит все необходимые скрипты в наш проект.
В views/site/about.php запишем следующий код:
<?php
/* @var $this yii\web\View */
$this->title = 'My Angular Application';
?>
<base href="/">
<div class="site-about">
<div class="body-content">
<my-app>Loading ...</my-app>
</div>
</div>
При открытии страницы в тег <my-app> будет загружаться наше angular приложение.
Настройка Angular2
В приложении app/ создаем файл book.ts Внутри опишем модель:
export class Book {
id: number;
title: string;
src: string;
author: string;
description: string;
created_at: string;
}
Теперь напишем сервис, который будет работать с этой моделью book.service.ts.
Обязательно укажите в private booksUrl ссылку на ваше API (иногда можно использовать относительный путь!)
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Book } from './book';
@Injectable()
export class BookService {
private headers = new Headers({'Content-Type': 'application/json'});
private booksUrl = 'http://local.site/apibooks'; // URL to web api
constructor(private http: Http) { }
getData(): Promise<Book[]> {
return this.http.get(this.booksUrl)
.toPromise()
.then(response => response.json() as Book[])
.catch(this.handleError);
}
getDetail(id: number): Promise<Book> {
return this.http.get(`${this.booksUrl}/${id}`)
.toPromise()
.then(response => response.json() as Book)
.catch(this.handleError);
}
create(title: string, src: string, created_at: string, description: string, author: string): Promise<Book> {
return this.http
.post(this.booksUrl, JSON.stringify({
title: title,
src: src,
created_at: created_at,
description: description,
author: author
}), {headers: this.headers})
.toPromise()
.then(res => res.json() as Book)
.catch(this.handleError);
}
update(book: Book): Promise<Book> {
const url = `${this.booksUrl}/${book.id}`;
return this.http
.put(url, JSON.stringify(book), {headers: this.headers})
.toPromise()
.then(() => book)
.catch(this.handleError);
}
delete(id: number): Promise<void> {
const url = `${this.booksUrl}/${id}`;
return this.http.delete(url, {headers: this.headers})
.toPromise()
// .then(() => null)
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
}
Функция getData() вытащит нам весь список из RESTapi.
getDetail(id: number) по параметру ID вытащит конкретную запись в соответствии с описанной ранее моделью.
create(title: string, src: string, created_at: string) создание новой записи в базе данных, через API по запросу POST /books
update(book: Book) в эту функцию мы передаем модель класса Book, и методом “put” делаем обновление данных по API
delete(id: number) - по ID дергаем метод “delete” в нашем API
private handleError(error: any) - обработка ошибки, если такая появится
Цель этого файла - описать все основные функции, с которыми будут работать наши компоненты. Проще говоря, здесь содержится вся логика работы нашего приложения.
Создадим и опишем файл: books.component.ts, который будет дергать методы,описанные там, и сможет работать с нашим сервисом:
import { Component, OnInit } from '@angular/core';
import {BookService} from './book.service';
import {Book} from './book';
@Component({
selector: 'my-books',
templateUrl: './templates/books.html'
})
export class BooksComponent implements OnInit {
books: Book[];
constructor(
private _bookService: BookService,
) { }
ngOnInit () {
this.getBooks();
}
getBooks() {
this._bookService.getData().then(books => this.books = books);
}
}
При создании этого компонента мы использовали шаблон books.html. Создадим папку “templates” и поместим туда books.html.
<div class="container">
<div class="row">
<div class="col-md-12">
<p>
<a class="btn btn-lg btn-success" [routerLink]="['/books/create']">+ Добавить материал</a>
</p>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div *ngFor="let book of books" class="col-xs-6 col-md-2">
<a routerLink="/detail/{{book.id}}" class="thumbnail">
<img src = "{{book.src}}" style="width:200px;height:200px;">
</a>
</div>
</div>
</div>
В компоненте books.component.ts мы используем router, он формирует ссылки и открывает приложение с этого места при перезагрузки страницы. Создадим файл app-routing.module.ts и настроим его:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BooksComponent } from './books.component';
import { DetailComponent } from './detail.component';
import { CreateComponent } from './create.component';
import { UpdateComponent } from './update.component';
const routes: Routes = [
{ path: 'books', component: BooksComponent },
{ path: 'books/create', component: CreateComponent },
{ path: 'detail/:id', component: DetailComponent },
{ path: 'update/:id', component: UpdateComponent },
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
Также создадим страницу для детального отображения: detail.component.ts
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import {BookService} from './book.service';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { Router } from '@angular/router';
import {Book} from './book';
@Component({
selector: 'my-detail',
templateUrl: './templates/detail.html'
})
export class DetailComponent implements OnInit {
book: Book;
constructor(
private _bookService: BookService,
private route: ActivatedRoute,
private location: Location,
private router: Router
) { }
ngOnInit () {
this.getBook();
}
delete(book: Book): void {
this._bookService
.delete(book.id)
.then(() => {
this.router.navigate(['/books']);
});
}
getBook() {
this.route.params.switchMap((params: Params) => this._bookService.getDetail(+params['id']))
.subscribe(book => this.book = book);
}
goBack(): void {
this.location.back();
}
}
Здесь мы реализуем основные функции нашего сервиса
delete(book: Book) удаление материала. А после этого отправляем пользователя на страницу со списком материалов
getBook() - вывод списка материалов
goBack() - отправка пользователя назад
Добавим шаблон detail.html:
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-body">
<h2>{{ book.title }}</h2>
<p>{{ book.created_at }}</p>
<p>
<a class="btn btn-warning" routerLink="/update/{{book.id}}">Редактировать</a>
<a class="btn btn-danger" (click)="delete(book); $event.stopPropagation()">Удалить</a>
</p>
<p>
<img src = "{{ book.src }}" class="img-responsive img-rounded">
</p>
<p>{{ book.description }}</p>
<p><b>{{ book.author }}</b></p>
<p>
<a class="btn btn-default" [routerLink]="['/books']">back</a>
</p>
</div>
</div>
</div>
<div class="col-md-6"></div>
</div>
</div>
Добавим функцию для создания материалов, реализуем ее в компоненте: create.component.ts
import { Component } from '@angular/core';
import {BookService} from './book.service';
import { Location } from '@angular/common';
import {Book} from './book';
@Component({
selector: 'my-detail',
templateUrl: './templates/create.html'
})
export class CreateComponent {
book: Book;
constructor(
private _bookService: BookService,
private location: Location
) { }
add(title: string, src: string, created_at: string, description: string, author: string): void {
title = title.trim();
src = src.trim();
created_at = created_at.trim();
description = description.trim();
author = author.trim();
if (!title) { return; }
this._bookService.create(title, src, created_at, description, author)
.then(() => {
this.location.back();
});
}
}
Шаблон create.html:
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Создание нового материала</div>
<div class="panel-body">
<div class="form-group">
<label>Название</label>
<input class="form-control" placeholder="Введите название книги" #bookName />
</div>
<div class="form-group">
<label>Картинка</label>
<input class="form-control" placeholder="Ссылка на изображение" #bookSrc />
</div>
<div class="form-group">
<label>Дата</label>
<input class="form-control" placeholder="Введите дату публикации" #createDate />
</div>
<div class="form-group">
<label>Детальное описание</label>
<textarea class="form-control" placeholder="Детальное описание" #bookDescription ></textarea>
</div>
<div class="form-group">
<label>Автор</label>
<input class="form-control" placeholder="Автор" #bookAuthor />
</div>
<button (click)="add(bookName.value, bookSrc.value, createDate.value, bookDescription.value, bookAuthor.value);
bookName.value=''; bookSrc.value=''; createDate.value=''; bookDescription.value=''; bookAuthor.value=''"
class="btn btn-primary">Отправить</button>
<a [routerLink]="['/books']" class="btn btn-default">Назад</a>
</div>
</div>
</div>
<div class="col-md-6"></div>
</div>
</div>
По аналогии создадим update.component.ts:
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import {BookService} from './book.service';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import {Book} from './book';
@Component({
selector: 'my-detail',
templateUrl: './templates/update.html'
})
export class UpdateComponent implements OnInit {
book: Book;
constructor(
private _bookService: BookService,
private route: ActivatedRoute,
private location: Location
) { }
ngOnInit () {
this.getBooks();
}
getBooks() {
this.route.params.switchMap((params: Params) => this._bookService.getDetail(+params['id']))
.subscribe(book => this.book = book);
}
save(): void {
this._bookService.update(this.book);
}
goBack(): void {
this.location.back();
}
}
И шаблон update.html:
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Редактирование материала</div>
<div class="panel-body">
<div class="form-group">
<label>Название</label>
<input class="form-control" [(ngModel)]="book.title" (ngModelChange)="save()" placeholder="Название" />
</div>
<div class="form-group">
<label>Дата</label>
<input class="form-control" [(ngModel)]="book.created_at" (ngModelChange)="save()" placeholder="Ссылка на изображение" />
</div>
<div class="form-group">
<label>Картинка</label>
<input class="form-control" [(ngModel)]="book.src" (ngModelChange)="save()" placeholder="Ссылка на изображение" />
</div>
<div class="form-group">
<label>Детальное описание</label>
<textarea class="form-control" [(ngModel)]="book.description" (ngModelChange)="save()" placeholder="Детальное описание" ></textarea>
</div>
<div class="form-group">
<label>Автор</label>
<input class="form-control" [(ngModel)]="book.author" (ngModelChange)="save()" placeholder="Ссылка на изображение" />
</div>
<a (click)="goBack();" class="btn btn-default">Назад</a>
</div>
</div>
</div>
<div class="col-md-6"></div>
</div>
</div>
Теперь создадим самый главный компонент, который и будет запускать наш роутер: app.component.ts
import { Component } from '@angular/core';
import {BookService} from './book.service';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<router-outlet></router-outlet>
`,
providers: [BookService]
})
export class AppComponent {
title = 'Коллекция книг';
}
Остался последний штрих. Нужно прописать самый главный модуль, который будет содержать в себе все компоненты: app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { BooksComponent } from './books.component';
import { DetailComponent } from './detail.component';
import { CreateComponent } from './create.component';
import { UpdateComponent } from './update.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpModule } from '@angular/http';
@NgModule({
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpModule,
],
declarations: [
AppComponent,
BooksComponent,
CreateComponent,
UpdateComponent,
DetailComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Проверим наше приложение! Выполним сборку (находиться нужно в корне angular приложения):
ng build
либо (если вы не задавали в конфиге .angular-cli.json)
ng build --base-href /route/dist/
Ура, заработало! Также обратите внимание на адресную строку при переходе между страницами изменяется урл, который можно копировать и отправлять друзьям!
Облако тегов
Следующая статья
Типы навигации на сайте
Ни один современный сайт не сможет существовать без хорошо налаженной навигационной системы. Наличие системы навигации дает возможность посетителям визуально и быстро определить ценность информации, и оперативно ее найти, где бы она ни располагалась.