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. Игорь ТрубицынИгорь Трубицын

    Добрый день! Почему может возникать такая штука?
    OPTIONS http://rest.server.ru/films 403 (Forbidden) angular.js:10514
    XMLHttpRequest cannot load http://rest.server.ru/films. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://rest.client.ru’ is therefore not allowed access.

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

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

  2. max

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

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

  3. 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) { — тогда ошибки не будет, но и перезагрузка не произойдет. Это уже другая ошибка

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

      по ссылке выводится список фильмов;)

      1. Дмитрий

        Ага) Разобрался. Не поменял урл в app.js. Спасибо.

    1. Михаил

      Для этого есть библиотеки, нужно просто загуглить.

  4. Серега

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

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

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

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

  5. Алексей

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

  6. Анна

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

    1. Дмитрий

      Анна, если вы это исправили, скажите как?

      1. Дмитрий

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