Простое приложение на Yii2 и Angular 2 Часть2

  • 11 сентября 2017
  • 4617
0%

Помогло?

В прошлом уроке мы с вами познакомились с бэкэндом на 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/

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

 

Облако тегов

Следующая статья

Типы навигации на сайте

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

Сообщения: (2)

?
Публикуя сообщение, вы соглашаетесь с пользовательским соглашением
ММое имя10 месяца назад
Спасибо за статью! Делаю самые первые шаги в angular, за плечами небольшой опыт в yii2 и эта статья прям мое спасение ) Пока мало что понятно, но очень интересно ) Для Angular 11 пришлось кое что переделать, но все запустилось
Ответить
?
Публикуя сообщение, вы соглашаетесь с пользовательским соглашением
Скрыть ответы: (1)
ААдминистратор9 месяца назад
Спасибо. Очень приятно! Желаю удачи в развитии и совершенстве =)
Ответить

Читайте также