Yii2: Простое приложение c AngularJS фронтендом. Клиентская часть 2

На данный момент мы имеем готовое RESTful api приложение в серверной части и простое AngularJS приложение в клиентской части. Дело за малым, обеспечить связь второго с данными из первого.

Доработаем главный модуль app.js

Добавим описание модуля, обеспечивающего работу с данными о фильмах, yii2AngApp.film:

...
var yii2AngApp = angular.module('yii2AngApp', [
  'ngRoute',
  'yii2AngApp.site',
  'yii2AngApp.film'
]);
// рабочий модуль
var yii2AngApp_site = angular.module('yii2AngApp.site', ['ngRoute']);
var yii2AngApp_film = angular.module('yii2AngApp.film', ['ngRoute']);
...

Модель данных для фильмов

Создадим новый файл client/models/film.js, он будет отвечать за crud операции через rest. Функционально, он будет аналогом модели в yii2.

'use strict';
yii2AngApp_film.factory("services", ['$http','$location','$route', 
    function($http,$location,$route) {
    var obj = {};
    obj.getFilms = function(){
        return $http.get(serviceBase + 'films');
    }    
    obj.createFilm = function (film) {
        return $http.post( serviceBase + 'films', film )
            .then( successHandler )
            .catch( errorHandler );
        function successHandler( result ) {
            $location.path('/film/index');            
        }
        function errorHandler( result ){
            alert("Error data")
            $location.path('/film/create')
        }
    };    
    obj.getFilm = function(filmID){
        return $http.get(serviceBase + 'films/' + filmID);
    }

    obj.updateFilm = function (film) {
        return $http.put(serviceBase + 'films/' + film.id, film )
            .then( successHandler )
            .catch( errorHandler );
        function successHandler( result ) {
            $location.path('/film/index');
        }
        function errorHandler( result ){
            alert("Error data")
            $location.path('/film/update/' + film.id)
        }    
    };    
    obj.deleteFilm = function (filmID) {
        return $http.delete(serviceBase + 'films/' + filmID)
            .then( successHandler )
            .catch( errorHandler );
        function successHandler( result ) {
            $route.reload();
        }
        function errorHandler( result ){
            alert("Error data")
            $route.reload();
        }    
    };    
    return obj;   
}]);

Функции obj.getFilm, obj.createFilm, obj.updateFilm и другие, являются транспортом для соответствующих crud операций через rest. Для примера, код:

obj.createFilm = function (film) {
        return $http.post( serviceBase + 'films', film )
        ...
    };

Создает новый фильм путем запроса методом POST.

obj.getFilm = function(filmID){
        return $http.get(serviceBase + 'films/' + filmID);
    }

Получает данные фильма с заданным id методом запроса GET.

obj.updateFilm = function (film) {
        return $http.put(serviceBase + 'films/' + film.id, film )
         ...
    };

Вносит изменения в данные фильма методом запроса UPDATE.

obj.deleteFilm = function (filmID) {
        return $http.delete(serviceBase + 'films/' + filmID)
        ...    
    };

Удаляет запись с соответствующим id методом запроса DELETE.

Контроллер модуля yii2AngApp_film

Создадим новый файл client.local/controllers/film.js.

'use strict';
yii2AngApp_film.config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/film/index', {
        templateUrl: 'views/film/index.html',
        controller: 'index'
    })
    .when('/film/create', {
        templateUrl: 'views/film/create.html',
        controller: 'create',
        resolve: {
            film: function(services, $route){
                return services.getFilms();
            }
        }
    })
    .when('/film/update/:filmId', {
        templateUrl: 'views/film/update.html',
        controller: 'update',
        resolve: {
          film: function(services, $route){
            var filmId = $route.current.params.filmId;
            return services.getFilm(filmId);
          }
        }
    })
    .when('/film/delete/:filmId', {
        templateUrl: 'views/film/index.html',
        controller: 'delete',
    })
    .otherwise({
        redirectTo: '/film/index'
    });
}]);

yii2AngApp_film.controller('index', ['$scope', '$http', 'services', 
    function($scope,$http,services) {
    $scope.message = 'Everyone come and see how good I look!';
    services.getFilms().then(function(data){
        $scope.films = data.data;
    });    
    $scope.deleteFilm = function(filmID) {
        if(confirm("Are you sure to delete film number: " + filmID)==true && filmID>0){
            services.deleteFilm(filmID);    
            $route.reload();
        }
    };
}])
.controller('create', ['$scope', '$http', 'services','$location','film', 
    function($scope,$http,services,$location,film) {
    $scope.message = 'Look! I am an about page.';
    $scope.createFilm = function(film) {
        var results = services.createFilm(film);
    }  
}])
.controller('update', ['$scope', '$http', '$routeParams', 'services','$location','film', 
    function($scope,$http,$routeParams,services,$location,film) {
    $scope.message = 'Contact us! JK. This is just a demo.';
    var original = film.data;
    $scope.film = angular.copy(original);
    $scope.isClean = function() {
        return angular.equals(original, $scope.film);
    }
    $scope.updateFilm = function(film) {    
        var results = services.updateFilm(film);
    } 
}]);

Структура аналогична контроллеру site. Сначала описываем маршруты и обслуживающие их функции и представления.

Шаблоны для модуля yii2AngApp_film

Файлы шаблонов будем хранить в каталоге client/views/film. Шаблоны будут содержать html разметку и элементы данных нашего angularjs приложения.

client/views/film/index.html

<div>
    <h1>Каталог фильмов</h1>    
    <p>{{ message}}</p>
    <div ng-show="films.length > 0">
        <a class="btn btn-primary" href="#/film/create">
            <i class="glyphicon glyphicon-plus"></i> Добавить
        </a>
        <table class="table table-striped table-hover">
            <thead>
            <th>Название</th>
            <th>Режиссер</th>
            <th>Описание</th>
            <th>Год</th>
            <th style="width:80px;">Действия&nbsp;</th>
            </thead>
            <tbody>
                <tr ng-repeat="data in films">
                    <td>{{data.title}}</td>
                    <td>{{data.director}}</td>
                    <td>{{data.storyline}}</td>
                    <td>{{data.year}}</td>
                    <td>
                        <a class="btn btn-primary btn-xs" href="#/film/update/{{data.id}}">
                            <i class="glyphicon glyphicon-pencil"></i>
                        </a> 
                        <a class="btn btn-danger btn-xs" ng-click="deleteFilm(data.id)">
                            <i class="glyphicon glyphicon-trash"></i>
                        </a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
    <div ng-show="films.length == 0">
        Каталог фильмов пуст
    </div>
</div>

client/views/film/create.html

<div>
    <h1>Каталог фильмов: добавление фильма</h1>

    <p>{{ message}}</p>
    <form role="form" name="myForm">
        <div class= "form-group" ng-class="{error: myForm.title.$invalid}">
            <label> Название </label>
            <div>
                <input name="title" ng-model="film.title" type= "text" class= "form-control" placeholder="Название фильма" required/>
                <span ng-show="myForm.title.$dirty && myForm.title.$invalid" class="help-inline">Название обязательно</span>
            </div>
        </div>
        <div class= "form-group">
            <label> Описание </label>
            <div>
                <textarea name="storyline" ng-model="film.storyline" class= "form-control" placeholder= "Описание фильма"></textarea>
            </div>
        </div>
        <div class= "form-group" ng-class="{error: myForm.director.$invalid}">
            <label> Режиссер </label>
            <div>
                <input name="director" ng-model="film.director" type= "text" class= "form-control" placeholder="Режиссер" required/>
                <!--<span ng-show="myForm.director.$dirty && myForm.director.$invalid" class="help-inline">Режиссер обязателен</span>-->
            </div>
        </div>
        <div class= "form-group" ng-class="{error: myForm.year.$invalid}">
            <label> Год </label>
            <div>
                <input name="year" ng-model="film.year" type= "text" class= "form-control" placeholder="Год год выпуска" required/>
                <span ng-show="myForm.year.$dirty && myForm.year.$invalid" class="help-inline">Год обязателен</span>
            </div>
        </div>

        <a href="#/film/index" class="btn btn-default">Cancel</a>
        <button ng-click="createFilm(film);" 
                ng-disabled="myForm.$invalid"
                type="submit" class="btn btn-default">Submit</button>
    </form>
</div>

client/views/film/update.html

<div>
    <h1>Каталог фильмов: редактирование фильма</h1>

    <p>{{ message}}</p>
    <form role="form" name="myForm">
        <div class= "form-group" ng-class="{error: myForm.title.$invalid}">
            <label> Название </label>
            <div>
                <input name="title" ng-model="film.title" type= "text" class= "form-control" placeholder="Название фильма" required/>
                <span ng-show="myForm.title.$dirty && myForm.title.$invalid" class="help-inline">Название обязательно</span>
            </div>
        </div>
        <div class= "form-group">
            <label> Описание </label>
            <div>
                <textarea name="storyline" ng-model="film.storyline" class= "form-control" placeholder= "Описание фильма"></textarea>
            </div>
        </div>
        <div class= "form-group" ng-class="{error: myForm.director.$invalid}">
            <label> Режиссер </label>
            <div>
                <input name="director" ng-model="film.director" type= "text" class= "form-control" placeholder="Режиссер" required/>
                <!--<span ng-show="myForm.director.$dirty && myForm.director.$invalid" class="help-inline">Режиссер обязателен</span>-->
            </div>
        </div>
        <div class= "form-group" ng-class="{error: myForm.year.$invalid}">
            <label> Year </label>
            <div>
                <input name="year" ng-model="film.year" type= "text" class= "form-control" placeholder="Год выпуска" required/>
                <span ng-show="myForm.year.$dirty && myForm.year.$invalid" class="help-inline">Year Required</span>
            </div>
        </div>

        <a href="#/film/index" class="btn btn-default">Cancel</a> 
        <button ng-click="updateFilm(film);" 
                ng-disabled="isClean() || myForm.$invalid"
                type="submit" class="btn btn-default">Submit</button>
    </form>
</div>

Последние штрихи

Не забываем добавить ссылку на каталог в меню:

...
<ul class="nav navbar-nav navbar-right">
    <li><a href="#/"><i class="glyphicon glyphicon-home"></i> Главная</a></li>
    <li><a href="#/film/index"><i class="glyphicon glyphicon-film"></i> Каталог фильмов</a></li>
...

и подключить модель и контроллер каталога:

...
    <script src="models/film.js"></script>
    <script src="controllers/film.js"></script>
...

Готово

Проверяем работу нашего каталога фильмов:

Yii2 AngularJS simple application

Заключение

Задача минимум выполнена: мы можем легко добавлять, редактировать и удалять нужные элементы каталога. Конечно, это примитивное приложение, однако, оно было создано с целью показать, как легко и быстро, буквально за 20-30 минут, можно собрать современное html5 приложение с использованием Yii 2.0, AngularJS и немножко Twitter Bootstrap.

При подготовке использованы материалы:

  1. The Definitive Guide to Yii 2.0;
  2. AngularJS Developer Guide;
  3. How To Create Single Page Application in minutes! with AngularJs 1.3 and Yii 2.0.

21 thoughts on “Yii2: Простое приложение c AngularJS фронтендом. Клиентская часть 2

      1. Влад

        вообще неправильно маршруты в js-файлах хардкодить. а поменяется если что-то в url-менеджере. добавите/уберете суффикс, да мало ли что. и что все в js вручную переписывать? о_О

    1. nix Автор записи

      Похоже на проблему CORS. Если браузер хром — попробуйте другой.
      Проверьте, передается ли заголовок Access-Control-Allow-Origin в запросе. Пример.

  1. max

    Почему после добавления контроллера {{ message}} на главной странице (контроллер site) стал выводить message из index контроллера film $scope.message = ‘Everyone come and see how good I look!’;

    В чем может быть проблема?

  2. Sintezz

    Здравствуйте, всё заработало с первого раза.
    Но есть некоторые моменты.

    При удалении фильма ругается на то, что не определён роут.

    Консоль:

    ReferenceError: $route is not defined
    at r.$scope.deleteFilm (http://site.lock/controllers/film.js:48:17)
    at fn (eval at (http://site.lock/assets/angular/angular.min.js:216:110), :2:382)
    at Jc.(anonymous function).compile.d.on.e (http://site.lock/assets/angular/angular.min.js:257:177)
    at r.$get.r.$eval (http://site.lock/assets/angular/angular.min.js:133:446)
    at r.$get.r.$apply (http://site.lock/assets/angular/angular.min.js:134:175)
    at HTMLAnchorElement. (http://site.lock/assets/angular/angular.min.js:257:229)
    at Of (http://site.lock/assets/angular/angular.min.js:35:394)
    at HTMLAnchorElement.d (http://site.lock/assets/angular/angular.min.js:35:341)

    Подскажите пожалуйста как его определить

    1. Алла

      Ошибка возникает из-за того, что в film контроллере стоит тоже $route.reload(); в 46 строке, а он не прописан в объявлении контроллера. Надо его добавить вот так:
      yii2AngApp_film.controller(‘index’, [‘$scope’, ‘$http’, ‘services’, ‘$route’,
      function($scope,$http,services, $route) { — тогда ошибки не будет, но и перезагрузка не произойдет. Это уже другая ошибка

  3. Серега

    Подскажите как сделать авторизацию при таком подходе (полное разделение клиентской части на ангуляре и серверное на пхп)?

      1. nix Автор записи

        Общая схема такая:
        1. Клиентское приложение (angular) запрашивает у пользователя логин/пароль;
        2. Клиентское приложение, на основании логина и пароля, получает от сервера (yii2) авторизационный токен и сохраняет его;
        3. В каждый запрос к серверу, клиентское приложение включает авторизационный токен;
        4. На основании полученного авторизационного токена, сервер определяет пользователя и работает дальше/отказывает, если недостаточно прав или не верный токен.

        Для конкретной реализации можно взять https://jwt.io (есть библиотеки под все современные языки).
        В приложении Yii2 нужно для класса, реализующего \yii\web\IdentityInterface, реализовать метод findIdentityByAccessToken().

  4. Алексей

    Круто! То что нужно для изучения REST с помощью Angular и Yii2. Автору респект! Отличный материал! Правда кое где по коду замечал косячки, исправлял у себя и всё получилось! Спасибо!

  5. Анна

    Классный материал! Но вот есть один момент. Когда создаю вторую страницу и размещаю на ней данные из другой таблицы, но той же базы, то каталог с фильмами перестает грузиться. В консоли сразу видно, что всегда прогружаются данные для второй таблицы. Как это можно исправить?

      1. Дмитрий

        Получилось добавить еще одну таблицу. на другую вкладку.Проблема в том, что при добавлении в зависимость модулей, каждый последующий перезаписывает предыдущий. Исправлял так: в app.js убрал модуль yii2AngApp.film, из контроллера film.js перенес данные в site.js(Т.е прописать в site.js пути и функции для своего view). После создал модель site.js и данные из модели film.js перенес туда. Все, теперь site.js поддерживает по этому алгоритму добавление неограниченного количества таблиц, просто нужно дописывать пути в конфигах, добавлять новые контроллеры и закидывать в модель функции для новых таблиц.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *