Статьи

AngularJS в приложениях Drupal

Angular.js — это горячая новинка для разработки приложений на клиенте. Что ж, это уже не так уж и ново, но, несомненно, он по-прежнему горяч, особенно сейчас, когда его используют и поддерживают Google. Он выводит идею инфраструктуры JavaScript на совершенно новый уровень и предоставляет отличную основу для разработки многофункциональных и динамичных приложений, которые могут работать в браузере или как гибридные мобильные приложения.

logo_drupal

В этой статье я собираюсь показать вам изящный маленький способ использования некоторых из его магий на сайте Drupal 7. Простая функциональность, но достаточная для демонстрации того, насколько мощным является Angular.js, и потенциальных вариантов использования даже в мощных серверных средах PHP, таких как Drupal. Так что же мы делаем?

Мы собираемся создать блок, который перечисляет некоторые заголовки узлов. Большой возглас Однако эти заголовки узлов будут загружаться асинхронно с использованием Angular.js, и над ними будет текстовое поле для фильтрации / поиска узлов (также выполняется асинхронно). В качестве бонуса мы также будем использовать небольшой модуль Angular.js с открытым исходным кодом, который позволит нам просматривать некоторую информацию об узле в диалоговом окне, когда мы нажимаем на заголовки.

Итак, начнем. Как обычно, весь код, который мы пишем в руководстве, можно найти в этом хранилище .

Ингредиенты

Для этого нам понадобится следующее:

  • Пользовательский модуль Drupal
  • hook_menu() Drupal для создания конечной точки для запроса узлов
  • Функция темы Drupal, которая использует файл шаблона для визуализации нашей разметки
  • Пользовательский блок Drupal для вызова функции темы и размещения разметки там, где мы хотим
  • Небольшое приложение Angular.js
  • Для бонусной части, угловой модуль ngDialog

Модуль

Давайте начнем с создания пользовательского модуля под названием Ang . Как обычно, внутри папки modules/custom создайте файл ang.info :

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

ang.module файл ang.module который будет содержать большую часть кода, связанного с Drupal. Внутри этого файла (не забудьте про открывающий <?php ), мы можем начать с реализации hook_menu () :

 /** * 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; } } 

В hook_menu() мы объявляем путь ( api/node ), к которому может обращаться любой, у кого есть права на просмотр содержимого, и который будет возвращать вывод JSON, созданный в функции обратного вызова ang_node_api() . Последний получает один аргумент, то есть то, что находится в URL после объявленного нами пути: api/node/[some-extra-param] . Нам нужен этот аргумент, потому что мы хотим достичь 3 вещей с этой конечной точкой:

  1. вернуть список из 10 последних узлов
  2. вернуть узел с определенным идентификатором (например, api/node/5 )
  3. вернуть все узлы, которые имеют переданный параметр в заголовке (например, api/node/chocolate , где chocolate является частью одного или нескольких заголовков узлов)

И это то, что происходит во второй функции. Параметр проверяется в трех случаях:

  • Если он существует и является числовым, мы загружаем соответствующий узел и возвращаем массив с некоторой базовой информационной формой этого узла (помните, это будет в формате JSON)
  • Если он существует, но не является числовым, мы выполняем запрос к базе данных и возвращаем все узлы, заголовки которых содержат это значение
  • В любом другом случае (что по сути означает отсутствие параметра), мы запрашиваем базу данных и возвращаем последние 10 узлов (просто в качестве примера)

Очевидно, что этот обратный вызов может быть улучшен и консолидирован (обработка ошибок и т. Д.), Но в демонстрационных целях он будет работать просто отлично. Давайте теперь создадим тему, которая использует файл шаблона и пользовательский блок для его рендеринга:

 /** * 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; } /** * Implements template_preprocess_angular_listing(). */ function ang_preprocess_angular_listing(&$vars) { // Can stay empty for now. } 

Здесь есть четыре простые функции:

  1. Используя hook_theme (), мы создаем нашу тему angular_listing которая использует файл шаблона angular-listing.tpl.php который мы скоро создадим.
  2. Внутри hook_block_info () мы определяем наш новый блок, отображение которого контролируется внутри следующей функции.
  3. Используя hook_block_view (), мы определяем вывод нашего блока: рендеринг массива с использованием темы angular_listing которому прикреплены соответствующие файлы javascript и css. Из Google CDN мы загружаем файлы библиотеки Angular.js, внутри ang.js мы напишем нашу логику JavaScript, а в папке /lib/ngDialog у нас есть библиотека для создания диалогов. Вы должны загрузить последний и поместить его в модуль, следуя описанной структуре. Вы можете найти файлы в хранилище или на веб-сайте библиотеки .
  4. Последняя функция является препроцессором шаблона для нашего шаблона, чтобы убедиться, что переменные передаются ему (даже если мы на самом деле не используем их).

Как видите, это стандартный шаблонный код Drupal 7. Прежде чем включить модуль или опробовать этот код, давайте быстро создадим файл шаблона, чтобы Drupal не выдавал ошибку. Внутри файла angular-listing.tpl.php добавьте следующее:

 <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 и добавим в него наш javascript (поскольку оба они связаны):

 angular.module('nodeListing', ['ngResource', 'ngDialog']) // Factory for the ngResource service. .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) { // Initial list of nodes. $scope.nodes = Node.query(); // Callback for performing the search using a param from the textfield. $scope.doSearch = function() { $scope.nodes = Node.search({param: $scope.search}); }; // Callback to load the node info in the modal $scope.open = function(nid) { $scope.loadedNode = Node.get({param: nid}); ngDialog.open({ template: 'loadedNodeTemplate', scope: $scope }); }; }]); 

Хорошо. Теперь у нас есть все (убедитесь, что вы также добавили файлы ngDialog как было запрошено в ключе #attached массива, который мы написали выше). Вы можете включить модуль и разместить блок где-нибудь на видном месте, где вы можете его увидеть. Если все прошло хорошо, вы должны получить 10 названий узлов (если их так много) и поле поиска выше. При поиске AJAX-вызовы будут поступать на сервер к нашей конечной точке и возвращать названия других узлов. И если щелкнуть по ним, откроется диалоговое окно с названием узла и телом. Сладкий.

Но позвольте мне объяснить, что происходит на стороне Angular.js. Прежде всего, мы определяем приложение Angular.js под названием nodeListing с ngResource (службой Angular.js, отвечающей за связь с сервером) и ngDialog качестве его зависимостей. Этот модуль также объявлен в нашем файле шаблона как основное приложение, используя директиву ng-app .

Внутри этого модуля мы создаем фабрику для нового сервиса под названием Node который возвращает $resource . Последний фактически является соединением с нашими данными на сервере (серверная часть Drupal доступна через нашу конечную точку). В дополнение к используемым по умолчанию методам мы определяем еще один .search() который будет выполнять запрос GET и возвращать массив результатов (нам нужен новый, потому что .get() по умолчанию не принимает массив результатов ).

Ниже этой фабрики мы определяем контроллер под названием ListController (также объявленный в файле шаблона с помощью директивы ng-controller ). Это наш единственный контроллер, и его область действия будет распространяться на все шаблоны. Есть несколько вещей, которые мы делаем внутри контроллера:

  1. Мы загружаем узлы из нашего ресурса, используя метод query() . Мы не передаем никаких параметров, поэтому мы получим последние 10 узлов на сайте (если вы помните наш обратный вызов конечной точки, запрос будет сделан к /api/node ). Мы прикрепляем результаты к области действия в переменной, называемой nodes . В нашем шаблоне мы перебираем этот массив с помощью директивы ng-repeat и перечисляем названия узлов. Кроме того, мы создаем кнопку для каждого с директивой ng-click которая запускает обратный вызов open(node.nid) (подробнее об этом в пункте 3).
  2. Взглянув на шаблон выше этого списка, у нас есть элемент input, значение которого будет связано с областью действия с помощью директивы ng-model . Но используя директиву ng-change мы вызываем функцию в области видимости ( doSearch() ) каждый раз, когда пользователь вводит или удаляет что-то в этом текстовом поле. Эта функция определена внутри контроллера и отвечает за поиск в нашей конечной точке по параметру, который пользователь вводил в текстовом поле (переменная search ). По мере выполнения поиска результаты автоматически заполняют шаблон.
  3. Наконец, для бонусной части мы определяем метод open() который принимает идентификатор узла в качестве аргумента и запрашивает узел из нашей конечной точки. При нажатии кнопки эта функция обратного вызова открывает диалоговое окно, которое использует шаблон, определенный внутри <script> с идентификатором loadedNodeTemplate и передает ему текущую область контроллера. И если мы обратимся к файлу шаблона, мы увидим, что шаблон диалога просто выводит заголовок и тело узла.

Вывод

Вы можете увидеть объем кода, который мы написали для реализации этой изящной функциональности. Большинство из них на самом деле является образцом. Очень быстрый блок запроса узла, который доставляет результаты асинхронно со всеми его преимуществами. И если вы знаете Angular.js, вы можете представить себе возможность дальнейшего расширения опыта Drupal.

Теперь, вам интересно узнать больше о любви между Angular.js и Drupal? Вы сделали бы что-нибудь по-другому? Дайте нам знать в комментариях ниже!