Адаптивная карусель на AngularJS

от автора

Материал для начинающих.
       Наверное, каждый начинающий web-разработчик должен написать кривую, с кучей костылей, но свою карусель. Вот и настало мое время.
       Карусель — элемент web-интерфейса, который поочередно показывает пользователю заранее подготовленные слайды с информацией.

пример карусели

       Идея нашей карусели: красиво выстроить карточки на экране в два уровня при помощи css свойства z-index и поочередно менять css положение карточек с анимацией изменений при помощи свойства transition.
       CSS файл будет выглядеть следующим образом:

просмотреть CSS

el-carousel {   width: 100%;   margin: 0;   position: relative;   z-index: 20; } .el-carousel .el-card {   position: absolute;   background: rgba(141, 141, 141, 0.5);   border:1px #e0e0e0 solid;   border-radius:1px;   box-shadow: 0 0 0 4px rgba(107, 108, 40, 0.25), 0 0 0 5px rgba(183, 183, 183, 0.6);   -webkit-transition: all 1s ease-in-out;   -moz-transition: all 1s ease-in-out;   -o-transition: all 1s ease-in-out;   transition: all 1s ease-in-out;   z-index: 5;   opacity: 0.2;   cursor: pointer; } .el-carousel .sm-el-card-1 {   height: 65%;   width: 23%;   left: 6%;   bottom: 1%;   z-index: 20; } .el-carousel .sm-el-card-2 {   height: 70%;   width: 25%;   left: 2%;   bottom: 7%;   opacity: 0; } .el-carousel .sm-el-card-3 {   height: 70%;   width: 25%;   left: 2%;   bottom: 7%;   z-index: 20;   opacity: 0.8; } .el-carousel .sm-el-card-4 {   height: 84%;   width: 30%;   left: 35%;   bottom: 14%;   z-index: 20;   opacity: 1;   background: rgba(141, 141, 141, 0.7); } .el-carousel .sm-el-card-5 {   height: 70%;   width: 25%;   left: 73%;   bottom: 7%;   z-index: 20;   opacity: 0.8; } .el-carousel .sm-el-card-6 {   height: 70%;   width: 25%;   left: 73%;   bottom: 7%;   opacity: 0; } .el-carousel .sm-el-card-7 {   height: 65%;   width: 23%;   left: 70.5%;   bottom: 1%; } .el-carousel .sm-el-card-8 {   height: 77%;   width: 27%;   left: 33%;   bottom: 8%; } .el-carousel .sm-el-card-hide {   height: 77%;   width: 27%;   left: 33%;   bottom: 8%;   opacity: 0; } .el-carousel .md-el-card-4 {   height: 57%;   width: 14%;   left: 2%;   bottom: 7%;   z-index: 20;   opacity: 0.6; } .el-carousel .md-el-card-5 {   height: 71%;   width: 16%;   left: 20.5%;   bottom: 11%;   z-index: 20;   opacity: 0.8; } .el-carousel .md-el-card-6 {   height: 84%;   width: 19%;   left: 40.5%;   bottom: 14%;   z-index: 20;   opacity: 1;   background: rgba(141, 141, 141, 0.7); } .el-carousel .md-el-card-7 {   height: 71%;   width: 16%;   left: 63.75%;   bottom: 11%;   z-index: 20;   opacity: 0.8; } .el-carousel .md-el-card-8 {   height: 57%;   width: 14%;   left: 84%;   bottom: 7%;   z-index: 20;   opacity: 0.6; } .el-carousel .md-el-card-9 {   height: 57%;   width: 14%;   left: 84%;   bottom: 7%;   opacity: 0; } .el-carousel .md-el-card-10 {   height: 67%;   width: 15%;   left: 62%;   bottom: 7%; } .el-carousel .md-el-card-1 {   height: 78%;   width: 18%;   left: 38.7%;   bottom: 9.5%; } .el-carousel .md-el-card-2 {   height: 67%;   width: 15%;   left: 18.7%;   bottom: 7%; } .el-carousel .md-el-card-3 {   height: 57%;   width: 14%;   left: 2%;   bottom: 7%;   opacity: 0; } .el-carousel .md-el-card-hide {   height: 78%;   width: 18%;   left: 38.7%;   bottom: 9.5%;   opacity: 0; } .el-carousel .lg-el-card-1 {   height: 78.7%;   width: 15%;   left: 40.5%;   bottom: 10%; } .el-carousel .lg-el-card-2 {   height: 65%;   width: 13%;   left: 23.5%;   bottom: 7%; } .el-carousel .lg-el-card-3 {   height: 52%;   width: 11%;   left: 9%;   bottom: 3%; } .el-carousel .lg-el-card-4 {   height: 33%;   width: 7%;   left: 1%;   bottom: 4%;   z-index: 10;   opacity: 0.4; } .el-carousel .lg-el-card-5 {   height: 57%;   width: 12%;   left: 10%;   bottom: 7%;   z-index: 20;   opacity: 0.6; } .el-carousel .lg-el-card-6 {   height: 71%;   width: 14%;   left: 25%;   bottom: 11%;   z-index: 20;   opacity: 0.8; } .el-carousel .lg-el-card-7 {   height: 84%;   width: 16%;   left: 42%;   bottom: 14%;   z-index: 20;   opacity: 1;   background: rgba(141, 141, 141, 0.7); } .el-carousel .lg-el-card-8 {   height: 71%;   width: 14%;   left: 61%;   bottom: 11%;   z-index: 20;   opacity: 0.8; } .el-carousel .lg-el-card-9 {   height: 57%;   width: 12%;   left: 78%;   bottom: 7%;   z-index: 20;   opacity: 0.6; } .el-carousel .lg-el-card-10 {   height: 33%;   width: 7%;   left: 91.32%;   bottom: 4%;   z-index: 10;   opacity: 0.4; } .el-carousel .lg-el-card-11 {   height: 52%;   width: 11%;   left: 77%;   bottom: 3%; } .el-carousel .lg-el-card-12 {   height: 65%;   width: 13%;   left: 59.5%;   bottom: 7%; } .el-carousel .lg-el-card-hide {   height: 78.7%;   width: 15%;   left: 40.5%;   bottom: 10%;   opacity: 0; } 

       Так как карусель адаптивная — все параметры размеров и положения определяются в процентах.
       Итак, попробуем реализовать нашу карусель angular-way. Чтобы каждая карточка карусели имела уникальный шаблон и действие, воспользуемся директивой и фабрикой.

Рассмотрим фабрику
В замыкании будем хранить два массива:

  • list — массив html-шаблонов карточек,
  • action — массив действий при клике на карточку.

       Фабрика предоставляет два метода addCard и addAction, которые добавляют в массивы html-шаблон и функцию, которая должна выполниться при клике соответственно. Функция addCard принимает на вход второй, необязательный параметр, который при логическом значении true разрешает промис. Это своего рода костыль, о котором я расскажу немного позже.
       Отдельно отмечу безопасное подключение зависимостей. Не углубляясь в детали, имена подключаемых зависимостей менять нельзя, так как это полностью ломает код при его минификации. Поэтому в ангуляре предусмотрено безопасное подключение зависимостей — вместо функции фабрики/контроллера/директивы передается массив, первыми элементами которого указываются строковые имена зависимостей, а последним уже сама функция с подключенными зависимостями. В данном случае минификация кода нам уже не страшна, однако нужно не забывать, что порядок перечисления строковых зависимостей в массиве должен совпадать с порядком подключения зависимостей в функцию.

просмотреть код фабрики

'use strict';  (function () {   angular.module('carousel')     .factory('card', ['$q', function ($q) {       var list = [],         action = [],         done = $q.defer(); // добавляем новую карточку       function addCard(card, last) {         if(typeof card === 'object' && card.length > 0) {           list.push(card);           if(last) { done.resolve(); }         }       } // добавляем действие при клике на карточку       function addAction(foo) {         action.push(foo);       } // возвращаем методы наполнения и массивы карточек, и действий       return {         addCard: addCard,         addAction: addAction,         list: list,         action: action,         done: done.promise       };     }]); })(); 

Директива html-шаблона карточек
       Тут все просто. Для директивы установлен изолируемый скоуп с тремя пробрасываемыми параметрами через атрибуты элемента, на который установлена директива.
       Отмечу, что для удобства в директивах можно изменять имена пробрасываемых параметров. Ключом в объекте скоупа указывается удобное для использования имя, а в его поле, после указания типа пробрасывания (= ,@, &) — имя атрибута элемента. Если атрибут элемента двойной, например last-card, то в имени элемента он указывается в виде верблюжьей нотации lastCard.
       Параметр action пробрасывается через & — это означает, что на вход принимается выражение, в нашем случае — функция клика по карточке.
       Параметры item и last пробрасываются через =, т.е. на вход принимается определенное значение, которое может быть задано как явно в значении атрибута, так и определено переменной скоупа, в котором находится директива, и передано в атрибут элемента переменной.
       Данные параметры необходимы в том случае, когда мы не хотим создавать отдельный шаблон для каждой карточки, а делаем один шаблон и клонируем его с помощью директивы ng-repeat. В параметр item передается текущий объект данных, полученный при помощи ng-repeat. В параметр last передается специальное значение $last, которому директива ng-repeat присвоит true, если объект из рассматриваемого массива для клонирования — последний.
       Для получения html-шаблона (фактически внутреннего содержимого директивы) необходимо установить параметру transclude значение true. При этом в связывающей функции директивы (link) появится пятый параметр, который представляет из себя функцию трансклюзии. Отмечу, что назначение данной функции более широкое, но в данном случае она используется только для получения внутреннего содержимого директивы.
       Настало время обосновать применение костыля с явным определением последнего элемента клонирования $last. Дело в том, что пользовательская директива с transclude в паре с ng-repeat работает специфически. Это связано с последовательностью выполнения операций: первым делом angular клонирует шаблон элемента, затем выполняются остальные директивы с меньшим приоритетом, в том числе пользовательские, и только после этого клонированные шаблоны наполняются значениями из скоупа. Поэтому если явно не указать, что ng-repeat сделал свое дело — карусель не будет отображать карточки с содержимым клонированных элементов.

просмотреть код директивы

'use strict';  (function () {   angular.module('carousel')     .directive('elCard', ['card', function(card) {       return {         scope: {           action: '&cardAction',           item: '=elCard',           last: '=lastCard'         },         restrict: 'A',         transclude: true,         link: function(scope, elem, attr, ctrl, transclude) { // наполняем массив действий при клике на соответствующую карточку           card.addAction(scope.action); // наполняем массив элементов карточек           transclude(scope, function(item) {             card.addCard(item, scope.last);             elem.remove();           });         }       };     }]); })(); 

Директива карусели
       Директива строит нашу карусель по имеющимся шаблонам. Карусель имеет три варианта исполнения: на 3, 5 или 7 карт в первом ряду. По умолчанию выбрано 7 карточек, но предусмотрен пробрасываемый параметр elements для определения количества карт вручную. В зависимости от количества карточек определяется коэффициент пересчета высоты элемента карусели heihtCoeff.
   Директива построена на трех функциях:

  • changeHeight — пересчитывает высоту карусели в зависимости от ширины экрана. Данная функция выполняется не только при первом старте директивы, но и при срабатывании события изменения размера окна браузера.
  • makeCards — создает необходимое количество элементов карточек. У данной функции есть два нюанса:
    1. количество пользовательских карточек меньше требуемого для карусели. В данном случае недостающие карточки наполняются повторяющимися имеющимися шаблонами.
    2. количество пользовательских карточек больше требуемого. Лишним картам присваивается положение на заднем фоне карусели и устанавливается абсолютная прозрачность. По мере движения карусели эти карточки меняются местами с уже показанными и так по кругу.

  • moveCards — каждой карточке присвоена директива ng-class, которая присваивает элементу имя класса из строкового значения переменной. В нашем случае все классы, определяющие местоположение карточек, занесены в массив строковых элементов и функция всего лишь реализует продвижение «очереди» при помощи методов массива shift и push.

       Для приведения в действие и остановки предусмотрены вспомогательные функции runCarousel и stopCarousel, которые периодически выполняют moveCards при помощи $interval. При переходе на другую вкладку $interval продолжает свою работу, а css свойство transition — нет, что приводит к сбоям в работе карусели. Поэтому старт и остановка карусели привязаны к событиям смены активного окна. По окончании работы директивы не забываем отвязать всех слушателей.

просмотреть код директивы

'use strict';  (function () {   angular.module('carousel')     .directive('elCarousel', ['$window', '$compile', '$interval', 'card', function($window, $compile, $interval, card) {       return {         scope: {           elements: '=elCarousel'         },         restrict: 'A',         link: function($scope, elem) {           var cards = [],             action = [],             heightCoeff = ($scope.elements === 3) ? 2 : ($scope.elements === 5) ? 3.3 : 3.96,             cardAmount = ($scope.elements === 3) ? 8 : ($scope.elements === 5) ? 10 : 12;           $scope.card = []; // присваиваем элементу необходимый для работы класс           elem.addClass('el-carousel'); // выполняем подготовительные действия для обычных карточек и созданных при помощи ng-repeat           function preStartActions() {             if(card.list.length < 1) { return; }             cards = card.list;             action = card.action;             makeCards();           }           preStartActions();           card.done.then(preStartActions); // изменяем высоту элемента в зависимости от ширины           function changeHeight() {             var carouselWidth = elem.width(),               carouselHeight = carouselWidth / heightCoeff;             elem.css('height', carouselHeight);           }           angular.element($window).bind('resize', changeHeight);           changeHeight(); // создаем DOM элементы карточек из массива           function makeCards() {             elem.empty();             var k = 0,               cardNumber = (cards.length > cardAmount) ? cards.length : cardAmount,               numClass  = (cardAmount === 8) ? 'sm-' : (cardAmount === 10) ? 'md-' : 'lg-';             for(var i = 0; i < cardNumber; i++) {               var div = angular.element('<div ng-click="cardAction' + i +                 '()" class="el-card" ng-class="card[' + i + ']"></div>');               if(i < cards.length) {                 div.append(cards[i].clone());                 $scope['cardAction' + i] = action[i];               } else {                 div.append(cards[k].clone());                 $scope['cardAction' + i] = action[k];                 k = (k > cards.length - 2) ? 0 : k + 1;               }               $scope.card[i] = (i < cardAmount) ? numClass + 'el-card-' + (i + 1) : numClass + 'el-card-hide';               $compile(div)($scope);               elem.append(div);             }           } // перемещаем карточки в порядке очереди           function moveCards() {             var lastElem = $scope.card.shift();             $scope.card.push(lastElem);           } // старт/стоп карусели в зависимости от активности окна           var moveInterval;           runCarousel();           angular.element($window).bind('blur', stopCarousel);           function stopCarousel() {             $interval.cancel(moveInterval);           }           angular.element($window).bind('focus', runCarousel);           function runCarousel() {             moveInterval = $interval(moveCards, 2000);           }           elem.bind('$destroy', function () {             $interval.cancel(moveInterval);             angular.element($window).unbind('blur', stopCarousel);             angular.element($window).unbind('focus', runCarousel);             angular.element($window).unbind('resize', changeHeight);           });         }       };     }]); })(); 

       В результатате наш html код карусели может выглядеть следующим образом

 <div el-carousel="7">   <div ng-repeat="card in cardList" el-card="card" card-action="someAction(someParam)" last-card="$last">     <span>{{card.name}}</span>     <img class="image image-1" ng-src={{card.img}} alt={{card.alt}}/>   </div>   <div el-card card-action="otherAction(otherParam)">     <img class="image image-2" src="/app/ru/main/img/js.png" alt="javascript"/>   </div> </div> 

       Можно использовать как индивидуально написанные шаблоны, так и клонированные с помощью ng-repeat, а также комбинировать их вместе.

Спасибо за внимание, всем удачи.

ссылка на оригинал статьи http://habrahabr.ru/post/261847/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *