Страницы

суббота, 25 октября 2014 г.

AngularJS & Google Apps Script

Сказ про то, как я дружил AngularJS с Google Apps Script, в частности с сервисом по имени HTML Service. Сразу хочу признаться в том, что от перечисленных продуктов "корпорации добра" я не в восторге, а попытки их мисковать на мой взгляд можно охарактеризовать как "увлекательный досуг для мазохистов", но тем не менее...

Удовольствие сомнительное, но в моем случае использование Google Apps Script было необходимым условием задачи, а AngularJS в силу ряда причин, не имеющих прямого отношения к настоящему посту, пришелся очень даже кстати.
В общем если вы приняли решение встать на скользкий путь создания мэшапов на GAS и ангуляр, думаю освоение следующего материала может сократить количество неприятных ощущений, которые вы без сомнения испытаете в процессе применения озвученных выше инструментов.

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

... и состоит из:
- Код.js - серверный javascript-код
- index.html - html-код
- js.html - клиентский javascript-код
- css.html - css-код

Код.js - html-шаблон отдаем обязательно в "нативном" режиме "песочницы", для инжекта кода, в нашем случае из файлов js.html и css.html, используем функцию require (назовите ее на ваш вкус):
function doGet(e) {
  var t = HtmlService.createTemplateFromFile('index');
  return t.evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE);
}

function require(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

index.html - в самом начале инжектим код клиентских js и css:
<?!= require('css'); ?>
<?!= require('js'); ?>

<div class="container" ng-app="">
  <h1>Hello <span ng-bind="name"></span></h1>
  <input type="text" ng-model="name" placeholder="Enter your name">  
</div>

js.html - здесь обязательно ссылка на полный, не минифицированный, код ангуляра:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>

css.html - забутстрапим для красоты:
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">

Разворачиваем веб-приложение, тестим:

Ура, мы все-таки скрестили ангуляр и GAS! Дальше расскажу как получить данные сервера на клиенте.

Добавим в файл Код.gs функцию по имени getData:
function doGet(e) {
  var t = HtmlService.createTemplateFromFile('index');
  return t.evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE);
}

function require(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getData() {
  return [['a','b','c'],[1,'qwe','rty'],[2,'asd','fgh'],[3,'zxc','vbn']];
}

В контексте этого поста неважно как мы получили эти данные, главное что они есть.

Пытаемся отобразить данные, полученные с сервера в таблице - редактируем файл index.html:
<?!= require('css'); ?>
<?!= require('js'); ?>

<div class="container" ng-app="app" ng-controller="MainCtrl">  
  <h1>Hello <span ng-bind="name"></span></h1>
  <input type="text" ng-model="name" placeholder="Enter your name">  
  <table class="table table-hover">
    <thead>
      <tr>
        <th ng-repeat="key in heading">{{key}}</th>
      </tr>
    </thead>
    <tbody>      
      <tr ng-repeat="arr in list">
        <td ng-repeat="i in arr">{{arr[$index]}}</td>
      </tr>
    </tbody>
  </table>
</div>

Теперь самое интересное - редактируем файл js.html - получаем данные с сервера и втыкаем их в $scope.list приложения (заголовок таблицы нас пока не интересует):
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script>
  angular.module('app', []).controller('MainCtrl', function($scope) {
    google.script.run.withSuccessHandler(gotData).withFailureHandler(gotError).getData();
    $scope.heading = [];
    $scope.list = [];
    function gotData(data) {
      console.log('data: ' + JSON.stringify(data));
      $scope.list = data;
    }
    
    function gotError(e) {
      console.log('error: ' + e.message);
    }
  });
</script>

И ничего не случилось, хотя данные с сервера прилетели:

Пытаемся обновить данные - редактируем файл js.html - $scope.apply() :
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script>
  angular.module('app', []).controller('MainCtrl', function($scope) {
    google.script.run.withSuccessHandler(gotData).withFailureHandler(gotError).getData();
    $scope.heading = [];
    $scope.list = [];
    function gotData(data) {
      console.log('data: ' + JSON.stringify(data));
      $scope.list = data;
      $scope.$apply();
    }
    
    function gotError(e) {
      console.log('error: ' + e.message);
    }
  });
</script>

И получаем ошибку на клиенте:

Пчелы начали что-то подозревать :). Должен признать что следующая итерация заняла у меня не менее получаса, редактируем файл js.html еще раз:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script>
  angular.module('app', []).controller('MainCtrl', function($scope) {
    google.script.run.withSuccessHandler(gotData).withFailureHandler(gotError).getData();
    $scope.heading = [];
    $scope.list = [];
    function gotData(data) {
      console.log('data: ' + JSON.stringify(data));             
      console.log('data isExtensible: ' + Object.isExtensible(data));
      console.log('data isSealed: ' + Object.isSealed(data));
      console.log('data isFrozen: ' + Object.isFrozen(data));      
      $scope.list = data;
      $scope.$apply();
    }
    
    function gotError(e) {
      console.log('error: ' + e.message);
    }
  });
</script>


Обращаю внимание на скриншот - не знаю где и с какой целью происходит "заморозка" данных, короче ничего не остается кроме как растасовать данные, полученные с сервера в аргументе data функции gotData путем перебора свойств аргумента, как-то так:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script>
  angular.module('app', []).controller('MainCtrl', function($scope) {
    google.script.run.withSuccessHandler(gotData).withFailureHandler(gotError).getData();
    $scope.heading = [];
    $scope.list = [];
    function gotData(data) {
      console.log('data: ' + JSON.stringify(data));
      var heading = data[0], i = 0, hLen = heading.length, j, dLen = data.length, arr;
      for (; i < hLen; i += 1) {
        $scope.heading.push(heading[i]);
      }
      for (i = 1; i < dLen; i += 1) {
        arr = [];
        for (j = 0; j < hLen; j += 1) {
          arr.push(data[i][j]);
        }        
        $scope.list.push(arr);
      }      
      $scope.$apply();
    }
    
    function gotError(e) {
      console.log('error: ' + e.message);
    }
  });
</script>

Еще пара вариантов "разморозки" попроще:
data = JSON.parse(JSON.stringify(data));

data = angular.copy(data);

Только после подобных манипуляций мы можем рассчитывать на результат:

На этом тему мэшапа Google Apps Script и AngularJS считаю раскрытой. Кого заинтересовали подробности этого извращения - пишите в комменты, по возможности постараюсь ответить.

p.s.: Если приложение по ссылке не отрабатывает как ожидается - попробуйте обновить страницу в браузере - это Спарта Google :)