Блог экспериментатора инженера-разработчика: Infanty.
Я пишу how-to статьи на редкие темы или статьи обзоры - для себя и тех кто со мной работает.
Блог существует при поддержке: "Оккупационных сил Марса".

Angular.js – один из популярных JavaScript фрэймворков, реализующих MVC и разрабатываемый при поддержке Google. Он позволяет создавать динамический веб-приложения, используя возможности HTML со встроенным тестированием. Использование современных подходов таких как биндинг данных и внедрения зависимостей позволяют выиграть время за счёт компактности кода.

MVC в Angular.js обеспечивает чистое и тестируемое разделение между поведением (контроллер) и видом / представлением (HTML-шаблон). Контроллер — это обычный класс JavaScript, который трансплантирован в область видимости вида. Благодаря этому взаимодействие модели с контроллером и видом очень простое. Модель же представляет собой набор объектов и примитивов на которые ссылается объект области видимости ($scope). Это позволяет легко протестировать изолированный контроллер, так как можно просто создать экземпляр контроллера без вида, потому что между ними нет никакой связи.

Начнём разработку модуля для Drupal 7 использующего в своей работе Angular.js. Для этого в директории: /sites/all/modules создадим папку ang. Разместим в ней файл: ang.info следующего содержания:

name = Ang
description = Angular.js example on a Drupal 7 site.
core = 7.x 

После чего в папке модуля сосздадим файл ang.module и реализуем в нём URL по которому на основе запрошенных данных возвращаются данные определённой ноды:

/**
 * Implements hook_menu().
 */
function ang_menu() {
  $items = array();

  $items['api/node'] = array(
    'access arguments' => array('access content'),
    'page callback' => 'ang_node_api',
    'page arguments' => array(2),
    'delivery callback' => 'drupal_json_output'
  );

  return $items;
}

/**
 * API callback to return nodes in JSON format.
 *
 * @param $param
 * @return array
 */
function ang_node_api($param) {
  // If passed param is node id.
  if ($param && is_numeric($param)) {
    $node = node_load($param);
    return array(
      'nid' => $param,
      'uid' => $node->uid,
      'title' => check_plain($node->title),
      'body' => $node->body[LANGUAGE_NONE][0]['value'],
    );
  }
  // If passed param is text value.
  elseif ($param && !is_numeric($param)) {
    $nodes = db_query("SELECT nid, uid, title 
                       FROM {node} n 
                       JOIN {field_data_body} b ON n.nid = b.entity_id 
                       WHERE n.title LIKE :pattern 
                       ORDER BY n.created DESC LIMIT 5", 
                      array(':pattern' => '%' . db_like($param) . '%'))
               ->fetchAll();
    return $nodes;
  }
  // If there is no passed param.
  else {
    $nodes = db_query("SELECT nid, uid, title 
                       FROM {node} n 
                       JOIN {field_data_body} b ON n.nid = b.entity_id 
                       ORDER BY n.created DESC LIMIT 10")
               ->fetchAll();
    return $nodes;
  }
} 

В коде выше реализуется функционал который позволяет пользователю с правами на доступ к контенту сайта запросить URL вида: http://example.com/api/node/23 и получить Id ноды, Id автора ноды, заголовок ноды и содержимое поля body (одно из базовых полей содержащихся в базовых материалах Drupal 7 при базовой установке) из ноды в виде JSON. При этом если последний аргумент URL из примера будет не числовой, а текстовый, то будет произведена попытка загрузки ноды не по Id, а с помощью поиска совпадений переданного текста из параметра и существующих нод. Если параметр из примера не будет передан вообще, то будет загружено 10 последних нод. Так как код разрабатываемого модуля учебный, то в нём нет проверки на существование загружаемой ноды и логирования или вывода сообщения о том, что нода отсутствует.

Следующим шагом станет объявление (в файле ang.module) блока с помощью hook_block_info(), содержимое которого выводится с помошью hook_block_view(). В содержимом блока:

  • подключается JavaScript файлы Angular.js;
  • подключается дополнительная библиотека ngDialog для Angular.js (подключаются её JavaSript и CSS файлы);
  • подключается JavaScript файл модуля;
  • выводится содержимое шаблона заданного с помошью hook_theme().
/**
 * Implements hook_theme().
 */
function ang_theme($existing, $type, $theme, $path) {
  return array(
    'angular_listing' => array(
      'template' => 'angular-listing',
      'variables' => array()
    ),
  );
}

/**
 * Implements hook_block_info().
 */
function ang_block_info() {
  $blocks['angular_nodes'] = array(
    'info' => t('Node listing'),
  );

  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function ang_block_view($delta = '') {
  $block = array();

  switch ($delta) {
    case 'angular_nodes':
      $block['subject'] = t('Latest nodes');
      $block['content'] = array(
        '#theme' => 'angular_listing',
        '#attached' => array(
          'js' => array(
            'https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js',
            'https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular-resource.js',
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog.min.js',
            drupal_get_path('module', 'ang') . '/ang.js',
          ),
          'css' => array(
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog.min.css',
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog-theme-default.min.css',
          ),
        ),
      );
      break;
  }

  return $block;
}

Создадим в папке модуля шаблон angular-listing.tpl.php (который задаётся в hook_theme()) следующего содержания:

 <div ng-app="nodeListing">
  <div ng-controller="ListController">
    <h3>Filter</h3>
    <input ng-model="search" ng-change="doSearch()">
      
    <ul>
      <li ng-repeat="node in nodes">
        <button ng-click="open(node.nid)">Open</button> {{ node.title }}
      </li>
    </ul>
    
    <script type="text/ng-template" id="loadedNodeTemplate">
      <h3>{{ loadedNode.title }}</h3>
      {{ loadedNode.body }}
    </script>
  </div>
</div> 

Данный шаблон содержит HTML код совмещённый с разметкой для  Angular.js, а так же секцию: script необходимую для ngDialog.

Последним шагом модуля станет создание в его папке файла ang.js для связи URL созданного на первом щаге и содержимого блока для вывода информации используя разметку шаблона блока.

 angular.module('nodeListing', ['ngResource', 'ngDialog'])

  .factory('Node', function($resource) {
    return $resource(Drupal.settings.basePath + 'api/node/:param', {}, {
      'search' : {method : 'GET', isArray : true}
    });
  })

  .controller('ListController', ['$scope', 'Node', 'ngDialog', function($scope, Node, ngDialog) {
    $scope.nodes = Node.query();

    $scope.doSearch = function() {
      $scope.nodes = Node.search({param: $scope.search});
    };

    $scope.open = function(nid) {
      $scope.loadedNode = Node.get({param: nid});
      ngDialog.open({
        template: 'loadedNodeTemplate',
        scope: $scope
      });
    };
  }]); 

Разберём данный JavaScript код подробней, так как код разрабатываемого модуля является учебным:

  • В первой строке определено Angular.js - app (модуль, приложение), с именем: nodeListing. Это приложение зависит от модулей (приложений, apps): ngResource и ngDialog.  Где ngResource обеспечивает поддержку взаимодействия с RESTful веб-службами позволяя создать CRUD (сокр. от англ. create, read, update, delete) приложение. Данный код взаимосвязан с первой строкой шаблона блока.
  • С третьей по седьмую строку производится связывание (mapping) объекта $resource с RESTful веб-службой с помошью метода factory. Данной связке даётся имя: Node. Связывание производится с URL реализованном в модуле Drupal, при вызове которого на основе запрошенных данных возвращаются данные определённой ноды.
  • В девятой строке определяется контролер (controler) с именем ListController. Он зависит от: контекста текущего приложения (области видимости), Node (см. предшествующий пункт) и ngDialog. Данный код взаимосвязан со второй строкой шаблона блока с помошью директивы (directive) ng-controller.
  • В десятой строке: при инициализации контроллера, производится заполнение переменной nodes в рамках текущего приложения данными полученными в результате обращения к RESTful веб-службе (URL реализованном в модуле Drupal) через mapping: Node. Данный код взаимосвязан со строками с седьмой по девятую в шаблоне блока в которых производится перебор массива nodes и вывод id ноды (в составе HTML кнопки), а так же вывод заголовка ноды.
  • С двенадцатой по четырнадцатую строку в контроллере задана функция связывающая RESTful веб-службу через mapping: Node и параметр mapping-а search с четвёртой строкой шаблона блока в которой содержится поле ввода. В этом поле (в шаблоне блока) произведена привязка к данной функции, а также реализованна передаа содержимого поля в переменную контекста search, которая потом используется для передачи в параметр mapping-а.
  • В шестнадцатой строке задана ещё одна функция конроллера которая взаимосвязана с 8 строкой шаблона блока (кнопкой с Id ноды). Функция получает Id ноды и обращается к RESTful веб-службе через mapping: Node с параметром в виде этого Id ноды, поле чего - результат сохраняется в переменную контекста: loadedNode (которая выводится в шаблоне блока в строках: тринадцать и четырнадцать с помошью выражения (expression). После этого производится вызов функции open из состава ngDialog с передачей имени шаблона для вывода (с двенадцатой по пятнадцатую строку шаблона блока) и контекста приложения.

P.S.: В виду интеграции Angular.js с Drupal, один из возможных подходов (для упрощения архитектуры Drupal модулей) - переносить команты CRUD с уровня HTTP (HTTP Methods: POST, GET, PUT, PATCH, DELETE) на уровень URLDrupal). Это позволяет каждую CRUD операцию RESTful веб-службы (в Drupal) реализовать в отдельном небольшом модуле (в случае если логика их обработки очень большая).

P.P.S.: В случае использования модуля для Drupal - Services, для защиты от CSRF можно воспользоваться следующим кодом:

 $scope.token = $http({method: 'GET', url:'/services/session/token'}).success(
  function(data, status, headers, config){
    $http.defaults.headers.post['X-CSRF-Token'] = data;
  }
); 

P.P.P.S.: Ссылки для самостоятельного изучения: