Набор методов для работы со списками в AngularJS

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

    Демка, песочница (с демкой играются многие, так что данные могут скакать)

    Как видно из примера, у нас проблема: куча списков со схожей функциональностью (добавление, удаление, сортировка элементов — что еще может быть у списков :-).

    Сервис


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

    Для нашей задачи используется наиболее простой тип: фабрика:

    angular.module('oi.list', [])
      .factory('oiList', function () {
                
        return function (scope, Resource) {
          scope.items = Resource.query()
          scope.add = Resource.add()
          ...
        }
      }
    

    Теперь, внедряя наш сервис в контроллер, получаем функцию, которая записывает в область видимости все необходимые методы.

    .controller('MyCtrl', ['$scope', 'ListRes', function ($scope, ListRes) {
    
        oiList($scope, ListRes);
    }])
    


    Кэширование


    Хорошо, но можно еще улучшить. При получении данных аяксом ($resource, $http) Ангуляр по-умолчанию кеширует полученные данные. Это означает, например, что загрузив в ng-view страницу с данными, уйдя с нее и снова вернувшись не придется загружать данные заново, т.к. они берутся из кэша.

    К сожалению, это работает только в элементарных случаях. Ангуляр кэширует именно запросы, а не модель. Т.е. загрузив и закэшировав массив данных с помощью Resource.query(), Ангуляр не возьмет данные из кэша если запросить их для отдельного элемента с помощью resArr[0].get(), потому что запрос будет уже другим. Так как кэш никак не связан с моделью, то его обновление при обновлении модели превращается в нетривиальную задачу.

    Для решения этих проблем добавим в приложение сервис oiListCache типа value. В нем будет храниться ссылка на модель. Если при загрузке данных видим, что ссылка пустая, загружаем с сервера, иначе берем модель по ссылке.

    .value('oiListCache', {cacheRes: {}})
    .factory('oiList', ['oiListCache', function (oiListCache) {
                
        return function (scope, cache, Resource) {
          if (angular.equals(oiListCache.cacheRes[cache], [])) {
           //Загружаем данные с сервера и записываем в кэш
            scope.items = oiListCache.cacheRes[cache] = Resource.query();
    
          } else {
            //Загружаем данные из кэша
            scope.items = oiListCache.cacheRes[cache];
          }
        }
      }
    

    Для каждой модели используем характеризующую ее строку cache, чтобы разные модели имели бы раздельный кэш.

    Методы


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

    Лучший способ — показать новый элемент сразу, а к базе привязать после получения ответа. И тут кроется большой подвох. Что делать, если пользователь удалил элемент у которого пока нет айдишника? Или одновременно добавил несколько элементов? В таком случае использую счетчик добавляемых/удаляемых элементов. При отправке запроса на добавление/удаление счетчик увеличивается, при получении ответа уменьшается. Код приводить не буду, его легко найти в песочнице.

    Известные проблемы


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

    1. В качестве параметров принимаются объект Resource и ключ для кэша cache. Если бы из ресурса можно было бы вытащить его имя, то оно бы отлично заменило ключ кэша. К сожалению, не представляю как его достать.

    2. При каждом изменении списка новое расположение элементов отправляется на сервер функцией sort(). Проблема в том, что без scope.$$phase || scope.$apply() отправка изменений происходит через раз.

    3. Сейчас модель записывается в область видимости под именем scope.items, которое нельзя поменять на другое. Выносить имя отдельной настройкой в параметры не хочется. Хочется в контроллере писать $scope.modelname = oiList($scope, 'list', ListRes), но при этом ломается биндинг, т.к. при получении данных с сервера не происходит их прямое присвоение области видимости.

    Поделиться публикацией

    Комментарии 11

      0
      Насчет третьей проблемы — Вы имеете в виду вложенность некоторых scope друг в друга?
      Если проблема в этом — зачем Вам сервис, когда можно сделать директиву с isolate-scope?
        0
        Дело не в этом. $scope один и тот же, просто в моем случае используется напрямую внутри функции (передаю как параметр). Но хотелось бы в функции работать не со $scope, а с внутренним объектом, который затем можно было бы присвоить $scope.

        Сейчас делается так:
        function oiList (scope) {
        scope.items = 'model'
        }
        oiList($scope)

        Хочется так:
        scope.items = function oiList () {
        return 'model'
        }
          0
          scope._ = new Smth();

          function oiList(smth){
          smth.items = 'asdf';
          };

          Такой вариант не устраивает? У каждой scope свой внутренний объект. Каждый лист работает с этим внутренним объектом, заполняя его поля.
            0
            Не очень понимаю, что это за подчеркивание. В нем мы теперь будем хранить модель данных?
              0
              Ага. Я использую этот вариант. Контроллер в моем случае (да и в вашем тоже) — это функция, в которой к scope привязывается новый объект, в котором находятся все свойства, необходимые для работы в данной области видимости.

              Плюсы:
              — можно использовать this без боязни потери контекста
              — можно сделать обычный класс, в котором есть какие-то функции, и инжектировать в него зависимости через контроллер.
              — не перекрываются области видимости в случае вложенных scope.

              Минусы — дополнительный уровень абстракции.

              Хотя мне кажется, что это именно Ваш случай.

              Пример:
              angular.controller('MyCtrl', ['$scope', 'some_resource', '$oiList', function($s, s_r, oiList){
                  $s._ = new oiList($s, s_r, 'modelName');
              }])
              

              oiList можно оформить как класс, scope внутри него можно использовать исключительно для специальных функций типа $apply, $emit, $eval и тд. Ну или ради функций в $rootScope.
                0
                Ага, понял! Подумывал о подобной штуке. Вполне себе вариант. Плюс в том, что всё изолированно. Минус — придется в шаблонах обращаться к функциям и данным через _.

                Если уйти от нюансов и посмотреть свысока, как бы вы решили подобную задачу? Т.е. организовали библиотеку функций для работы со списками?
                  0
                  Именно библиотеку — каким-нибудь классом, внутри которого были бы необходимые функции.
                  В случае ангуляра этот класс выносится в отдельный модуль, который можно впоследствии подключать.

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

                  Быть может, Вам поможет вот эта статья.
                  А может, Вам нужен provider, а не factory.

                  Минус — придется в шаблонах обращаться к функциям и данным через _.

                  Это не такой уж большой минус на самом деле. Можно вместо "_" написать «innerScope» или еще что-нибудь в этом духе. "_" был выбран как минимальный более-менее адекватный идентификатор внутреннего объекта. Меньше символов — больше функциональности. =)
                    0
                    Быть может, Вам поможет вот эта статья.

                    Извиняюсь, не посмотрел, что она Ваша. =)
                      0
                      Есть такое)
                      0
                      А если не библиотеку? У вас на сайте два списка с методами добавления/удаления элементов. Это уже повод подумать над вынесением общего кода.

                      Над провайдером думал, но пока не ясно что он может дать кроме предварительной настройки…
                        0
                        Только что пришел в голову такой вариант:
                        1. Сделать какой-нибудь сервис(фабрика или провайдер, неважно)
                        2. Внутри этого сервиса написать класс с логикой
                        3. Написать функцию, которая будет всегда конструировать новый объект(по сути, фабрика)
                        4. Внутри контроллера при инжектировании всегда вызывать эту функцию.

                        В итоге будет что-то вроде этого:
                        app.factory('smth', function(){
                          return function(){
                            createNewService: function (dependencies){
                                return new MyCoolService(dependencies);
                            }
                          }
                        
                         var MyCoolService = (function () {
                                        function MyCoolService() {...};
                                            
                                        MyCoolService.prototype.coolFunction = function () {
                                           ...
                                        };
                                        return MyCoolService;
                                    })();
                        })
                        .controller('SmthCtrl', ['$scope', 'smth', function($s, smth){
                            $s._ = smth.createNewService($s);
                        }])
                        


                        Код по классам нагло скопировал из сгенерированного TypeScript'ом, поэтому могут быть косяки.

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

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