AngularJS – перевод курса от CoodSchool

в 13:49, , рубрики: AngularJS, codeschool, javascript, обучение

Данная публикация является переводом оригинального курса CoodSchool с небольшими дополнениями, которые показались мне уместными в данном контексте. Публикация рассчитана на тех, кто только начинает знакомится с Angular.

Введение

AngularJS — популярная JavaScript библиотека, предназначенная, главным образом, для создания одностраничных веб-приложений с динамически обновляемым содержимым. Библиотека была написано словацким программистом Мишкой Хевери, который, работая в Google, получил от своего шефа задание выучить JavaScript. Он решил, что лучший способ изучить язык — это создать на его основе собственный фреймворк. Angular (звучит как «Ангула» и дословно переводится как «Угловатый») использует концепцию разделения данных и их представления, известную как MVC (M — model (данные), V — view (представление), С — controller (контролер, управляющая прослойка между ними обеспечивающая логику работы приложения).

Разные источники описывают назначения контролера либо так, что контролер реагирует только на действия пользователя и вносит изменения в модель, а потом эти изменения проявляются в интерефейсе без его участия. Либо так, что контролер не только вносит изменения в модель, но и отвечает за их получение от модели вместе с последующей обработкой по необходимости. Angular придерживается второго концепта.

Angular не зависит от внешyих библиотек и написан на чистом JavaScript (об этом часто упоминается как о плюсе). При этом, как правило, его рекомендуют использовать в связке с Bootstrap CSS. Так же Angula не ограничивает совместное использование с JQuery, например, для визуальных эффектов. В принципе, и любые друге JS библиотеки, нацеленные на представление (дизайн), например, React от команды Facebook, могут использовать совместно с Angular без особого конфликта между собой.

1. Знакомимся с основными сущностями фреймворка: модуль, контролер, директива, выражение

Базовой структурой в Angular является модуль. По сути это контейнер для взаимосвязанных функций. Поэтому создание приложения на Angular начинается с создания базового модуля, в котором мы указываем имя приложения. Прежде всего создадим файл app.js, который будет содержать следующий код:

var app = angular.module('AppName', [ ]);

AppName — имя нашего приложения. Оно, конечно, может быть любое другое на ваш вкус. В квадратных скобках [ ] указываются зависимости на другие модули, необходимые для работы нашего модуля. Если их нет, указываем просто пустой массив. Подробнее об этом позже.

Контролер [Controler]
Далее в модуле мы определяем контролер. Пока он один, но их может быть и много. Просто называем каждые другим именем.

app.controller('StoreController', function(){
 
    this.data = {name: 'Gem',  // это те данные которые
                 price : 3.49} // предоставляет контролер

});

Теперь несколько важных действий внутри HTML кода (которые свяжут наш модуль с дизайном). Для этого создадим файл index.html, в котором прежде всего объявим, что мы используем модуль с выбранным нами именем. Теперь Angular знает, какой модуль использовать внутри данного HTML. Так же подключим саму библиотеку и файл с нашим приложением.

<script type="text/javascript" src="angular.min.js"></script> 
<script type="text/javascript" src="app.js"></script>

Выражение [Expression]
Внутри HTML в фигурных скобках {{}} указывается переменная. Эта переменная предварительно должна быть определена в контролере. Так же внутри {{}} мы можем указать не только переменную, но и выражение. Например {{'Цена: '+store.data.price+'$'}}.

Обратите внимание, что выражения нельзя использовать внутри атрибута src тега IMG, поскольку браузер пытается загрузить изображение раньше, чем опеределен JS. Поэтому в Angular используется специальная директива ng-src:

<img ng-src="{{expression}}"/>

Директива [Directive]
Деректива специальный атрибут, который используется непосредственно внутри HTML тегов и обрабатывается Angular в соответствии с определенной для данной дерективы логикой. Например, внутри блока, где нам нужно отображать данные, мы указываем имя контролера (в данном случае StoreController), который будет нам эти самые данные предоставлять. Делается это при помощи встроенной дерективы ng-controller.

<div
 ng-controller="StoreController as store">
{{store.data.price}} // отобразится 3.49
<div>

Другими часто используемыми встроенными директивами являются:
ng-show — если ее значение является False скрывает HTML элемент (используя display:none). Полезно когда нужно убрать элементы интерфейса в зависимости от состояния модели. Например для товара который продан автоматически убирать кнопку Купить.
ng-hide — обратный аналог ng-show. Т.е. скрывает элемент если его значение равно True.
ng-repeat — аналогичен по смыслу функции foreach. Т.е. циклично повторяет включенные в блок HTML теги. Например: добавим в контролер еще один продкут:

app.controller('StoreController', function(){
 
    this.data = [{name: 'Gem', price : 3.49},  
                 {name: 'Emerald', price : 17.99}]

});

Теперь, для того чтобы вывести цены обоих продуктов, используем ng-repeat:

<div
 ng-controller="StoreController as store">
   <div
 ng-repeat="product in store.data">
      <li>{{product.name}}:  ${{product.price}}
   <div>
<div>

На выходе получим:
Gem: $3.49
Emerald: $17.99

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

2. Фильтры, дерективы и чистый код

Фильтры используются в выражениях и директивах и предназначены для изменения формата данных.

{{ data | filter[:option1][:option2] }} е

<li>{{product.name}}: {{product.price | currency:"$":0}} // Emerald: $17
                        ============= = ======== === =
                              |       |     |     |  +- количество знаков после запятой (опционально)
                              |       |     |     +- символ валюты (опционально)
                              |       |     +- фильтр (валюта)
                              |       +- pipe ("труба" означающая что данные перед использованием будут еще пропущены через фильтр)
                              +- цена

Примеры фильтров:

date:  
{{'1388123412323' | date:'MM/dd/yyyy @ h:mma'}} // 12/27/2013 @ 12:50AM
uppercase: 
{{'octagon gem' | uppercase}} // OCTAGON GEM
limitTo
{{'My Description' | limitTo:8}} // My Descr
<li ng-repeat="product in store.products | limitTo:3"> // ограничить список продуктов тремя
orderBy
<li ng-repeat="product in store.products | orderBy:'-price'"> // сортировка по цене

Полный список встроенных в Angular фильтров.

Теперь несколько слов о чистоте кода. Рассмотрим следующий пример:

<section ng-init="tab = 1">
 <ul class="nav nav-pills">
   <li ng-class="{active:tab === 1}">
     <a href ng-click="tab = 1">Description</a>
   </li>
   <li ng-class="{active:tab === 2}">
     <a href ng-click="tab = 2">Specifications</a>
   </li>
   <li ng-class="{active:tab === 3}">
     <a href ng-click="tab = 3">Reviews</a>
   </li>
 </ul> 
<div class="panel" ng-show="tab === 1"> 
   <h4>Description </h4>
 <p>{{product.description}}</p>

</div>
</section>

Как видно, логика находится непосредственно в HTML, что конечно не правильно и ее следует переместить на уровень контролера. Для этого добавим в функцию конторлера следующий код:

app.controller("PanelController", function(){
  // инициализация 
  this.tab = 1;

  // установка выбранного табулятора
  this.selectTab = function(setTab) { 
    this.tab = setTab;
  }; 

  // проверка если текущий табулятор выбран
  this.isSelected = function(checkTab){
    return this.tab === checkTab;
  };
});

Логика теперь находится внутри контролера и осталось только соответственно изменить HTML:

<section ng-controller="PanelController as panel">
 <ul class="nav nav-pills">
   <li ng-class="{ active: panel.isSelected(1) }">
     <a href ng-click="panel.selectTab(1)">Description</a>
   </li>
   <li ng-class="{ active: panel.isSelected(2) }">
     <a href ng-click="panel.selectTab(2)">Specifications</a>
   </li>
   <li ng-class="{ active: panel.isSelected(3) }">
     <a href ng-click="panel.selectTab(3)">Reviews</a>
    </li>
 </ul> 
<div class="panel" ng-show="panel.isSelected(1)"> 
  <h4>Description </h4>
 
  <p>{{product.description}}</p>

</div>
</section>

3. Модели и Валидаторы на примере создания формы

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

<form name="reviewForm">
  <blockquote>
    <b>Stars: {{review.stars}}</b>
 
              {{review.body}} 

    <cite>by: {{review.author}}</cite> 

  </blockquote>
  <select ng-model="review.stars">
    <option value="1">1 star</option>
 
    <option value="2">2 stars</option>

    . . .

   </select>
   <textarea ng-model="review.body"></textarea>
   <label>by:</label>
   <input ng-model="review.author" type="email" />
   <input type="submit" value="Submit" />
 
</form>

Так, например, изменение данных в селекторе автоматически отобразится на странице при помощи выражения {{review.stars}}

Таким образом, данные с уровня представления (View) передаются на уровень хранения данных (Model) и обратно.

Так же нужно отметить, что при использовании дерективы ng-model происходит валидация данных, установка специальных классов по итогам валидации и множество других полезных событий.

Теперь свяжем форму с контролером, который обеспечит её обработку. Для начала создадим сам контролер, содержащий метод addReview, который должен быть вызван при отправке формы.

app.controller("ReviewController", function(){

  this.review = {}; // объект для данных формы
  this.addReview = function(product) {
    product.reviews.push(this.review); // добавим данные из формы в массив результатов
    this.review = {}; // очистим форму 
  };
});

После чего внесем необходимые коррективы в форму.

<form name="reviewForm" ng-controller="ReviewController as reviewCtrl"
                        ng-submit="reviewCtrl.addReview(product)">
  <blockquote>
    <b>Stars: {{reviewCtrl.review.stars}}</b>
 
              {{reviewCtrl.review.body}} 

    <cite>by: {{reviewCtrl.review.author}}</cite> 

  </blockquote>
  <select ng-model="reviewCtrl.review.stars">
    <option value="1">1 star</option>
 
    <option value="2">2 stars</option>

    . . .

   </select>
   <textarea ng-model="reviewCtrl.review.body"></textarea>
   <label>by:</label>
   <input ng-model="reviewCtrl.review.author" type="email" />
   <input type="submit" value="Submit" />
 
</form>

Теперь добавим валидацию. Прежде всего отметим поля формы как обязательные при помощи аргумента required, а так же добавим условие, чтобы данные сохранялись только для валидной формы. Для этого в тег Form добавим опцию novalidate, которая выключит дефолтную HTML валидацию и добавим условие reviewForm.$valid, благодаря которому метод addReview будет вызываться только у валидной формы.

<form name="reviewForm" ng-controller="ReviewController as reviewCtrl" novalidate
                        ng-submit="reviewForm.$valid && reviewCtrl.addReview(product)">
  <blockquote>
    <b>Stars: {{reviewCtrl.review.stars}}</b>
 
              {{reviewCtrl.review.body}} 

    <cite>by: {{reviewCtrl.review.author}}</cite> 

  </blockquote>
  <select ng-model="reviewCtrl.review.stars" required>
    <option value="1">1 star</option>
 
    <option value="2">2 stars</option>

    . . .

   </select>
   <textarea ng-model="reviewCtrl.review.body" required></textarea>
   <label>by:</label>
   <input ng-model="reviewCtrl.review.author" type="email" required/>
   <div> reviewForm is {{reviewForm.$valid}} </div>
   <input type="submit" value="Submit" />
 
</form>

Все поля определенного типа, указанного при помощи аттрибута type, будут проверятся на предмет корректного заполнения. При этом Angular будет добавлять к полю определенный CSS класс в зависимости от состояния валидации.

Так, например, для типа email установленные классы будут меняться следующим образом:

.ng-pristine .ng-invalid // начальное состояние (чисто, невалидно)
.ng-dirty .ng-invalid // невалидный е-мейл (заполнено, невалидно)
.ng-dirty .ng-valid // валидный е-мейл (заполнено, валидно)

Соответственно мы можем установить для данных классов любые подходящие нам CSS правила. Так, например, вокруг некорректно заполненного поля будет красная рамка, а вокруг корректно заполненного зеленая.

.ng-dirty.ng-invalid {
   border-color: #FA787E;
 // красная рамка
}
.ng-dirty.ng-valid {

   border-color: #78FA89;
 // зеленая рамка
}

В настоящий момент для поля Input поддерживаются следующие валидаторы:

  • time
  • date
  • datetime-local
  • week
  • month
  • number
  • text
  • email (соответствие минимальным требованиям)
  • url (соответствие минимальным требованиям)
  • checkbox
  • radio

4. Создание собственных деректив

Одной из великолепных особенностей Angular является возможность создания собственных деректив. Другими словами, можно создавать собственные HTML-теги, которые могут иметь собственную логику и заменять собой целые куски HTML. Можно, например, создать тег , вставив который мы получим на странице редактор текста.

Это позволяет писать выразительный код, который полностью передает структуру нашего приложения. И конечно, это позволяет сильно улучшить повторное использование кода.

В принципе, что касается повторного использования фрагментов HTML кода, Angular имеет в своем распоряжении директиву ng-include, которая позволяет вынести часть кода в отдельный файл. Выглядит это следующим образом:

<h3 ng-include="'product-title.html'"></h3>

Обратите внимание на одиночные кавычки вокруг имени. Они указывают Angular, что это строка с именем, а не переменная, содержащая имя файла.

Соответственно в файле product-title.html будет находится код, который Angular вставит как содержимое тега H3. Например:

{{product.name}} 

 <em class="pull-right">${{product.price}}</em>

Как видно, вставляемый таким образом код может содержать выражения, которые будут обработаны после вставки такого фрагмента в общий код.

Но вернемся обратно к созданию собственных деректив. Делается это следующим образом:

app.directive('productTitle', function(){
 
  return { 
    restrict: 'E', // E - означает что деректива предназначена для Element
    templateUrl: 'product-title.html' 
  };
 
});

Теперь в мы можем использовать это как HTML тег. Обратите внимание, что имя директивы productTitle в HTML превратится в product-title.

<product-title></product-title>

Другой способ кастомизации тега является создание кастомного аттрибута. В этом случае тег вместо E(Element) при создании директивы нам нужно указать A(Attribute) для свойства restrict. После этого HTML тег будет выглядеть следующим образом:

<h3 product-title></h3>

Рекомендуется использовать restrict: E для вставки виджетов и других полностью самостоятельных элементов, содержащих собственную логику, а restrict: А — для примесей (mix-in).

В случае, когда речь идет о виджете, нам не обойтись без контролера, который будет содержать всю необходимую логику. Ангулар позволяет реализовать это несколькими способами. Прежде всего можно просто добавить аттрибут ng-controller:

<product-panels ng-controller=«PanelController as panels»>

Но более правильно будет использовать свойство controller внутри функции, создающей нашу директиву:

app.directive('productPanels', function(){
 
  return {
    restrict: 'E',
    templateUrl: 'product-panels.html',
    controller: function(){
      ... // логика 
    },
    controllerAs: 'panels' // название контролера
  };
 
});

5. Зависимости и Сервисы (Dependencies and Services )

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

// файл app.js
(function(){

  var app = angular.module('store', []);
      app.controller('StoreController', function(){ . . . });
      app.directive('productTitle', function(){ . . . });
      app.directive('productGallery', function(){ . . . });
      app.directive('productPanels', function(){ . . . });
})();

Логично было бы вынести директивы в самостоятельный модуль или даже для каждой создать свой собственный модуль, в зависимости от необходимой гибкости.

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

// файл products.js
(function(){

 var app = angular.module('store-products', []);
     app.directive('productTitle', function(){ . . . });
     app.directive('productGallery', function(){ . . . });
     app.directive('productPanels', function(){ . . . });
})();

Теперь укажем модуль store-products как зависимость для модуля store:

// файл app.js
(function(){

 var app = angular.module('store', [store-products]);
     app.controller('StoreController', function(){ . . . });
})();

Последним шагом нужно не забыть подключить наш новый файл:

// файл index.html
<!DOCTYPE html>
 <html ng-app="store">

<head> . . . </head>

 <body ng-controller="StoreController as store">

. . .

 <script src="angular.js"></script>

 <script src="app.js"></script>
 <script src="products.js"></script>

</body>
 </html>

В итоге мы разделили модуль на основе их функциональности.

app.js — содержит топ-левел модуль подключаемый при помощи директивы ng-app;
products.js — содержит всю функциональность для продуктов и только продуктов.

Так же в качестве зависимостей мы можем указывать встроенные в Ангулар сервисы, которые предоставляют различный стандартный функционал. Имена встроенных сервисов начинаются с $.

В качестве примера наиболее популярных сервисов можно привести следующие:

$http — коммуникация со сервером посредством XMLHttpRequest;
$log — логирование сообщений в консоль браузера;
$filter — фильтрация данных в массиве;
полный список.

Пример включения зависимостией (dependency injection) в контролер.

app.controller('SomeController', [ '$http', '$log', function($http, $log){
 

  var store = this; 
      store.products = [];
  $http({ method: 'GET', url: '/products.json' }).success(function(data, status, headers, config) {
    $log.info(status);
    store.products = data; 
  })
} ]);

Автор: non4me

Источник


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