Pull to refresh

AngularJS 1.x – перевод курса от CodeSchool

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

Введение


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

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

Angular не зависит от внешних библиотек и написан на чистом JavaScript (об этом часто упоминается как о плюсе). При этом, как правило, его рекомендуют использовать в связке с Bootstrap CSS. Так же Angular не ограничивает совместное использование с 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, в котором прежде всего объявим, что мы используем модуль с выбранным нами именем <html ng-app=«AppName»>
Теперь 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 ("труба" означающая что данные перед использованием будут еще пропущены через фильтр)
                              +- цена


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

<b>date</b>
{{'1388123412323' | date:'MM/dd/yyyy @ h:mma'}} // 12/27/2013 @ 12:50AM


<b>uppercase</b>
{{'octagon gem' | uppercase}} // OCTAGON GEM


<b>limitTo</b>
{{'My Description' | limitTo:8}} // My Descr
<li ng-repeat="product in store.products | limitTo:3"> // ограничить список продуктов тремя


<b>orderBy</b>
<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 является так называемое двусторонние связывание (Two-way Data Binding), означающие, что изменение данных на странице (в форме) автоматически изменяет данные в модели (объекте хранящем данные), а изменение данных в модели так же автоматически обновляет связанную информацию на странице. На первый взгляд выглядит немного запутано, поэтому стоит пояснить на примере:

<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>


Так, например, изменение данных в селекторе <select ng-model=«review.stars» > автоматически отобразится на странице при помощи выражения {{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 класс в зависимости от состояния валидации.

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

.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. Можно, например, создать тег <editor>, вставив который мы получим на странице редактор текста.

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

В принципе, что касается повторного использования фрагментов 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; 
  })
} ]);
Tags: angularjscodeschoolобучениеjavascriptангулар
Hubs: JavaScript Angular
Total votes 50: ↑44 and ↓6 +38
Comments 20
Comments Comments 20

Popular right now