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

в 12:59, , рубрики: AngularJS, Grid, javascript

Здраствуйте! Хотел бы поделиться с вами разработкой подтаблиц для нашего веб проекта. Цель заключалась в воссоздании веб модуля, имитирующего таблиц и подтаблиц (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 запросов я использовал промисы, и задал каждому из них различный таймаут).

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

Демо

Автор: alik_explore

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js