Воссоздание подтаблицы в таблице на AngularJS

Здраствуйте! Хотел бы поделиться с вами разработкой подтаблиц для нашего веб проекта. Цель заключалась в воссоздании веб модуля, имитирующего таблиц и подтаблиц (subdatasheet) созданных на базе Аccess. Наш клиент привык работать на Access'e, но времена меняются, и теперь наша задача заключается в гладком переходе на веб платформу, с минимальной разницей.


Почему AngularJS?


Имея не много опыта с различными javascript библиотеками, пришел к выводу что AngularJS изначально принуждает вашему проекту быть маленьким, чистым, изолированным и легко расширяемым. Также, используя directive со своим изолированным скопам (scope), дает возможность многоразового применения, даже внутри себя. Что и будет продемонстрировано под катом.


Как наш итоговый directive будет применяться


Так как в нашем проекте будет много таких подтаблиц, нам нужно сделать удобным нашу утилиту в применении. Должно быть примерно таким:


<div ng-controller="ctrl1">
    <subgrid config="config1"> </subgrid>
</div>

Посмотрите демо здесь. Кому интересно прошу под кат.



.controller('ctrl1', function(){
   ///...

   $scope.config1 = {
    t1:{
      subgrid:true,
      width:300, 
      height:200,
      config:[
        {
          title:"Filed 1",
          map:"field1"
        },
        {
          title:"Filed 2",
          map:"field2"
        },
        {
          title:"Filed 3",
          map:"field3"
        },
        {
          title:"Filed 4",
          map:"field4"
        },
        {
          title:"Filed 5",
          map:"field5"
        },
        {
          title:"Filed 6",
          map:"field6"
        }
        ],
        t:"",
        load:function(id, idx){
          $p.ajax($mock.data1,200).then(function(d){
            $scope.config1.t2.init(d); 
          },function(d){
            $scope.config1.t2.timeout(d);
          });
        }

      },
    t2: {
      subgrid:true,
      width:200, 
      height:100,
      config:[
      {
        title:"Filed 1",
        map:"field1"
      },
      {
        title:"Filed 2",
        map:"field2"
      }
      ],
      t:"",
      load:function(id, idx){
        $p.ajax($mock.data1,200).then(function(d){
          $scope.config1.t3.init(d); 
        },function(d){
            $scope.config1.t3.timeout(d);
          });
      }
    },
    t3: {
      subgrid:false,
      width:200, 
      height:100,
      config:[
      {
        title:"Filed 1",
        map:"field1"
      },
      {
        title:"Filed 2",
        map:"field2"
      }
      ],
      t:""
    }
  };

   /// ---
});

Все просто! Задаем директив, присоединяем объект конфигурации к нему, и таблица с гнездом подтаблиц готова.


Теперь о самом коде


Рассмотрим наш директив:


.directive('subgrid', ['$timeout','$compile',function($timeout,$compile) {
    return {
      restrict: 'E',
      scope: {
        config: '=',
        count: '='
      },

      templateUrl: 'subgrid.html',
      link: function(scope, elem, attr, ngModelCtrl) {
          scope.endrender=function(){
            $timeout(function(){
              scope.render = false;
            },1);
          }
          scope.expanded = false;
          scope.expandedid = null;
          scope.cnt = scope.count?scope.count:1;
          scope.cnf = scope.config["t"+scope.cnt];
          scope.guid = guid();
          scope.$watch('cnf.t',function() {
              scope.render = true;
          }, true);
          scope.cnf.timeout = function(error){
              scope.cnf.subgrid = false;
              scope.cnf.config = [{title:"Message",map:"field1"}];
              scope.cnf.t = {RowCount:1, field1:[error],index:[1]};
          }
          scope.cnf.init = function(d){
            scope.cnf.t = "";
            $timeout(function(){
              scope.cnf.t = d;
            },1);
          }

          scope.expander = function(id, idx){
            //if not same row
            if(scope.cnf.subgrid)
            if(id!==scope.expandedid){
                angular.element(elem[0].querySelector("#"+scope.expandedid)).children().eq(0).children().text("+");
                angular.element(elem[0].querySelector("#"+scope.guid+'sub')).remove();
                scope.expandedid = id;
                var count = scope.cnt + 1;
                var tr = angular.element(elem[0].querySelector("#"+id));
                tr.children().eq(0).children().text("-");
                var exid = scope.guid+'sub';
                tr.after($compile("<tr id='"+exid+"'><td colspan ='{{cnf.config.length+1}}' style='padding:10px;'><subgrid count='"+count +"' config='config'></subgrid></td></tr>")(scope));
                if (typeof scope.cnf.load === "function") { 
                    scope.config["t"+count].t = "";
                    scope.cnf.load(id, idx);
                }
                scope.expanded = true;
            }
            else{
              if(scope.expanded){
                angular.element(elem[0].querySelector("#"+id)).children().eq(0).children().text("+");
                angular.element(elem[0].querySelector("#"+scope.guid+'sub')).remove();
                scope.expanded = false;
                scope.expandedid = null;
              }
              else{
                scope.expanded = true;
                scope.expandedid = id;
                var count = scope.cnt + 1;
                var tr = angular.element(elem[0].querySelector("#"+id));
                tr.children().eq(0).children().text("-");
                var exid = scope.guid+'sub';
                tr.after($compile("<tr id='"+exid+"'><td colspan ='{{cnf.config.length+1}}' style='padding:10px;'><subgrid count='"+count +"' config='config'></subgrid></td></tr>")(scope));
                if (typeof scope.cnf.load === "function") { 
                    scope.config["t"+count].t = "";
                    scope.cnf.load(id, idx);
                }
              }
            }
          }

          function guid() {
              function s4() {
                return Math.floor((1 + Math.random()) * 0x10000)
                  .toString(16)
                  .substring(1);
              }
              return "id"+s4() + s4();

          }

      }
    };
  }]);

Пройдусь по порядку в крации:


{
      restrict: 'E',
      scope: {
        config: '=',
        count: '='
}

И так, лимитируем тип нашего директива как элемент Е. Нет нужды делать его разно-типным, чтоб было меньше конфузии.


config:'=', count: '='

нужны для инъекции объектов из контроллера (controller) в определенный директив.


scope.endrender=function(){
            $timeout(function(){
              scope.render = false;
            },1);
          }
          scope.$watch('cnf.t',function() {
              scope.render = true;
          }, true);

endrender используется для того чтобы спрятать процесс строения самой таблицы, так как запоздалость запросов с дальних серверов дает некрасивые эффектные последствия, которые легче заменить красивым спинером. scope.render активируется после того как последняя строка таблицы сконструировалась. scope.$watch слушает каждое изменение таблицы и деактивирует scope.render до его окончания.


scope.expander = function(id, idx){
            //if not same row
            if(scope.cnf.subgrid)
            if(id!==scope.expandedid){
                angular.element(elem[0].querySelector("#"+scope.expandedid)).children().eq(0).children().text("+");
                angular.element(elem[0].querySelector("#"+scope.guid+'sub')).remove();
                scope.expandedid = id;
                var count = scope.cnt + 1;
                var tr = angular.element(elem[0].querySelector("#"+id));
                tr.children().eq(0).children().text("-");
                var exid = scope.guid+'sub';
                tr.after($compile("<tr id='"+exid+"'><td colspan ='{{cnf.config.length+1}}' style='padding:10px;'><subgrid count='"+count +"' config='config'></subgrid></td></tr>")(scope));
                if (typeof scope.cnf.load === "function") { 
                    scope.config["t"+count].t = "";
                    scope.cnf.load(id, idx);
                }
                scope.expanded = true;
            }
            else{
              if(scope.expanded){
                angular.element(elem[0].querySelector("#"+id)).children().eq(0).children().text("+");
                angular.element(elem[0].querySelector("#"+scope.guid+'sub')).remove();
                scope.expanded = false;
                scope.expandedid = null;
              }
              else{
                scope.expanded = true;
                scope.expandedid = id;
                var count = scope.cnt + 1;
                var tr = angular.element(elem[0].querySelector("#"+id));
                tr.children().eq(0).children().text("-");
                var exid = scope.guid+'sub';
                tr.after($compile("<tr id='"+exid+"'><td colspan ='{{cnf.config.length+1}}' style='padding:10px;'><subgrid count='"+count +"' config='config'></subgrid></td></tr>")(scope));
                if (typeof scope.cnf.load === "function") { 
                    scope.config["t"+count].t = "";
                    scope.cnf.load(id, idx);
                }
              }
            }
          }

          function guid() {
              function s4() {
                return Math.floor((1 + Math.random()) * 0x10000)
                  .toString(16)
                  .substring(1);
              }
              return "id"+s4() + s4();

          }

expander является главным хэндлером при развертки таблицы на подтаблицу. Главной фишкой для генерирования новой подтаблицы, это динамически внедрять наш собственный элемент внутри себя. Но стоит заметить, что при внутреннем использовании мы добавляем атрибуту count. Это для того чтобы различать какую таблицу из нашей конфигурации config1, директив должен использовать какую таблицу t1, t2 и т.д..


guid просто напросто назначает уникальный ID на каждую строку в каждой таблице. Чтоб мы уверенно могли менять/удалять нужную нами строку в нужной нами таблице.


Шаблон


<div class="t-datasheet" ng-class="{'spinner':render}" ng-style="{'width':cnf.width+'px','height':cnf.height+'px'}">
<table ng-hide="render">
  <thead >
    <tr>
        <td>#</td>
        <td  ng-repeat="c in cnf.config" ng-cloak>{{c.title}}</td>
    </tr>
  </thead>
  <tbody >

   <tr id="{{guid+i}}" ng-repeat="i in cnf.t.index" ng-init="($last && endrender())">
       <td ng-click="expander(guid+i,i)" ><span ng-show="cnf.subgrid">+</span></td>
       <td ng-repeat="c in cnf.config" ng-cloak>{{cnf.t[c.map][i-1]}}</td>
    </tr>
  </tbody>
</table>
</div>

Шаблон получился простым и динамичным, где таблица имеет возможность динамически расширять столбцы по мере конфигурации config1.


Все остальное что не описано здесь, это утилиты использованные для тестирования этого кода (например: вместо реальных http запросов я использовал промисы, и задал каждому из них различный таймаут).


Данный проект является прототипом моей дальнейшей разработки. И делюсь с целью узнать о ваших мнениях и советах как усовершенствовать его.


Демо

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    +2
    Простите за занудство, но после выхода angular 1.6 не следует использовать ng-controller, $scope и директивы, сегодня лучше использовать компоненты, плюс очень много кода можно заменить просто темплейтом, так же очень рекомендую почитать

    https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md

    очень дельные советы по поводу хорошего стиля angular приложения с пояснениями, почему так.
      0

      Спасибо за ссылку. Вы правы насчет Angular 2, но на данный момент наша база построена на старой версии. А рефакторить займет не мало времени.

        +1
        Вы не сможете отрефакториться на ng2, придется все переписывать
        0
        Уточню, что компоненты появились в 1.5
          0

          Надо же даже не и не знал ), Похож ли он на концепт Angular 2?
          Спасибо

            0
            Подход один и тот же, но я бы не назвал это концептом. Сейчас мы используем связку Typescript + AngularJS 1.5, что очень похоже на Angular 2
              +1
              Как можно писать на ангуляре, писать по нему статьи и при этом не читать его release notes?
          0
          Имея не много опыта с различными javascript библиотеками, пришел к выводу что AngularJS изначально принуждает вашему проекту быть маленьким, чистым, изолированным и легко расширяемым.


          В голос. Далее в статье идёт тонна костылей с раздуванием ангуляровской абстракции, повествуя о том, как левой рукой почесать правое ухо. Почему не jQuery? Не «модно»?
            0

            При всем уважении к jQuery, в моем малом опыте я увидел большую разницу при рефакторинге одного из модулей написанных на jQuery. Tам, я заботился о каждом элементе в ручную, что делает мой код огромным по мере возрастания, особенно если у меня много параметров (смотря что вы пишите). В angular'e двух стороннее оповещение делает все за меня. Также, все мои итерации для менюшек и таблиц перешли в сам шаблон. А так, angular достаточно дружелюбен с jQuery, и они прекрасно работаю вместе. Даже если вы не подключите jQuery в ваш проект, Angular имеет свой встроенный jQLite с необходимыми методами. link

            0
            Вот, действительно,
            принуждает вашему проекту быть маленьким, чистым, изолированным и легко расширяемым
            — вы серьезно? Посмотрите в сторону React, что составить для себя полную картину, и иметь полное представление.
              0

              имхо. React не пробовал, но наслышен много

                0
                Рекомендую для расширения кругозора. Вы удивитесь насколько легко и элегантно решается описанная вами задача.
                  +2
                  На angular эта задача решается не менее легко и красиво, просто автор поста решил выстрелить себе в ногу.
                +1
                Посмотрите в сторону React

                — вы серьезно? React прекрасен сам по себе, но на практике бесполезен без redux, redux-thunk, react-redux. Любое простейшее действие вроде fetch'а данных текущего пользователя превращается в ад с написанием кода в 5(!) файлах (условно — actionType, reducer, action, ComponentContainer, Component). Это по Вашему маленький, чистый и понятный код?
                  0
                  Что-то я не понял, что вам мешает фетчить данные прямо в компоненте? Что вам мешает не использовать redux? Или Вы в проекте на ангуляре не разносите сущности по файлам?
                  Если использовать ng2, ngrx/store вместе с ngrx/effects это превращается в такой же ад. Различие лишь в том, что в реакте я могу себе выбрать другой котел, а в ng — нет.
                0
                $scope и директивы? Вы шагаете назад? уж 2017 на дворе
                  0
                  Components + $ctrl?

                Only users with full accounts can post comments. Log in, please.