Создайте директиву оболочки для animate.css

В этом руководстве будет представлен способ реализации директивы-оболочки для популярного файла анимации animate.css.

Вступление

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

Как описывает это пользователь github daneden: animate.css - это набор классных, забавных и кроссбраузерных анимаций, которые вы можете использовать в своих проектах. Отлично подходит для выделения, домашних страниц, ползунков и всего прочего "просто добавь удивительности".

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

Однако, если вы хотите анимировать страницу веб-сайта, вам может потребоваться немного больше, чем просто css. Например, некоторые анимации могут запускаться только тогда, когда связанный элемент страницы находится в области просмотра браузера. Кроме того, если вы хотите добавить много анимаций с настраиваемыми свойствами и вручную перезаписываете каждое свойство, все станет довольно запутанным.

По этим причинам я решил попробовать реализовать директиву оболочки, которая будет внутренне устанавливать имена анимации, события запуска и свойства времени для каждой анимации.

Стратегия

Начнем с обдумывания. Я понял, что такую ​​директиву можно реализовать двумя способами:

  1. Стандартный подход, который заключается в наличии одной директивы в качестве атрибута для каждого анимированного элемента DOM (например, анимации) с определенными связанными атрибутами, описывающими имя анимации и свойства, которые будут перезаписаны.
  2. Или следуя примеру директив ngMessages + ngMessage из модуля Angular ngMessages. Могут быть две директивы (например, анимация и анимация), сгруппированные следующим образом: одна или несколько директив animation, индивидуально прикрепленных к элементу DOM и содержащихся внутри директива анимации.

Обе стратегии имеют свои преимущества и недостатки.

Немного проанализировав контекст, в котором эта директива, скорее всего, будет использоваться, я решил выбрать стратегию, которая использует парные директивы.

Аргументация:

  • Может быть много отдельных элементов с одинаковой анимацией и одинаковыми настраиваемыми свойствами. Директива анимации может использоваться для сохранения общих свойств, и тогда директива анимации будет только отмечать элементы, которые должны быть оживленным. Это позволит пропустить много повторяющегося кода.
  • Может быть много отдельных элементов с множеством общих свойств и несколькими отличиями. Директива анимации может использоваться для сохранения общих свойств, а затем директива анимации предоставит конкретные свойства для каждого элемента. Это также позволит сохранить код в максимально короткой и удобной версии.
  • Директива animation также может перезаписывать любое свойство, установленное в директиве animations, в случае необходимости.

Настраиваемые свойства

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

Установка некоторых настраиваемых свойств (путем перезаписи css):

  • задержка эффекта анимации (сколько времени ждать до запуска анимации)
  • продолжительность эффекта анимации (как долго она продлится)
  • итерации эффекта анимации (сколько раз он будет повторяться подряд)

Добавляем еще поведение:

  • установите анимацию для элемента только когда элемент находится внутри области просмотра (он виден на экране).
  • установите анимацию только при первом появлении элемента в области просмотра.
  • отключить анимацию на небольших устройствах.

После небольшого исследования того, как выполнить часть улучшения поведения, я обнаружил Путевые точки.

Путевые точки - это простая библиотека, которая упрощает выполнение функции при переходе к элементу. Выполняемая функция может использоваться для присвоения классов элементу темы, поэтому она идеально подходит для установки классов анимации для элемента DOM, когда мы прокручиваем его. Верно, что у Waypoints есть и другие, более специализированные «дети», но мы не собираемся использовать их здесь.

Директива анимации

Эта директива будет использоваться как оболочка для анимированных элементов.

Мы хотим создать эту директиву в форме тега HTML и использовать ее для хранения общих свойств для внутренних директив animation.

Вот его превью:

angular
    .module('app')
    .directive('animations', animationsFunc);
function animationsFunc() {
    return {
      restrict: 'E',
      scope: {
          duration: '@',              // effect duration
          delay: '@',                 // effect delay
          iterations: '@',            // effect repeat times
          name: '@',                  // animation name
          trigger: '@',               // animation triggering event
          triggerOnce: '@',           // how many times to apply an effect when triggering event is fired
          triggerOffset: '@',         // trigger event when element is visible in a certain height position on the screen
          disableOnSmallDevices: '@'  // disable child elements animation on small devices
     }
     controller: ...
}

Атрибуты области привязаны к "@", что означает, что будет только односторонняя привязка данных.

Теперь добавьте в эту директиву контроллер. Цель контроллера - объединить указанные свойства с набором предопределенных и вернуть результат. Любая дочерняя директива будет иметь доступ к общим свойствам через этот контроллер.

controller: function($scope) {
  var defaultProperties = {
    duration: '2s',
    delay: '0s',
    iterations: '1',
    animation: '',
    trigger: '',
    triggerOnce: '',
    triggersCnt: 0,
    triggerOffset: '100%',
    disableOnSmallDevices: 'false'
  };
this.getProperties = function () {
    var foundProperties = {
      duration: $scope.duration,
      delay: $scope.delay,
      iterations: $scope.iterations,
      animation: $scope.name,
      trigger: $scope.trigger,
      triggerOnce: $scope.triggerOnce,
      triggerOffset: $scope.triggerOffset,
      disableOnSmallDevices: $scope.disableOnSmallDevices
    };
return _.merge({}, foundProperties, defaultProperties, function(foundProp) {
      if (foundProp) {
        return foundProp;
      }
    });
  }
}

Что касается слияния, я использую lodash 1.3.1, потому что он уже используется в других частях веб-сайта, но вы можете использовать все, что захотите, или реализовать слияние самостоятельно (что я не рекомендую).

Директива анимации

Эта директива позаботится о тяжелой работе, если она вообще есть.

Мы хотим, чтобы директива анимации присутствовала в форме атрибута в элементе DOM, который мы хотим анимировать. Для этого мы ограничим директиву «A».

Мы сказали, что контроллер animations будет возвращать общие свойства каждой директиве анимации через свой контроллер. Мы получаем контроллер, запрашивая его с помощью require:^ animations‘. Знак «^» указывает angular искать контроллер между родительскими элементами элемента.

У этой директивы также будет контроллер, который мы будем использовать для предоставления небольшого API в функцию связывания.

angular
  .module('app')
  .directive('animation', animationFunc);
function animationFunc($timeout) {
  return {
    restrict: 'A',
    require: '^animations',
    controller: function ($scope) {
      ...
    }
    link: function (scope, element, attrs, animationsCtrl) {
      ...
    }
  }
}

Мы также хотим иметь возможность перезаписывать любое свойство, установленное в директиве animations. Чтобы сделать это, мы могли бы аналогичным образом создать изолированную область (например, для анимации) и добавить туда имена свойств. Это звучит как хорошая идея, но поскольку директива является элементом, есть много шансов, что она будет размещена в элементе DOM, который уже имеет изолированную область видимости. В этом случае будет выдано сообщение об ошибке с текстом, похожим на «Несколько директив, запрашивающих новую / изолированную область на…».

Помня об этом случае, я решил составить заранее определенный список атрибутов и затем проверить его с помощью attrs [propertyName]. Чтобы получить окончательные свойства анимации для текущего элемента, я записал в контроллер следующую функцию:

function getFinalProperties(attrs, animationsCtrl) {
  var selfProps = {};
  var wantedProps = [
    'animation',
    'delay',
    'duration',
    'iterations',
    'trigger',
    'triggerOnce',
    'triggerOffset',
    'disableOnSmallDevices'
  ];
wantedProps.map(function(prop) {
    if (attrs.hasOwnProperty(prop) && attrs[prop]) {
      selfProps[prop] = attrs[prop];
    }
    return prop;
  });
return _.merge(animationsCtrl.getProperties(), selfProps);
}

Теперь мы можем применить окончательные свойства к каждому анимированному элементу, вызвав getFinalProperties () в верхней части функции ссылки.

Имея окончательные свойства, мы можем установить анимацию для соответствующего элемента вместе с его свойствами длительности, задержки и итераций (если они указаны). Вы можете найти инструкции, как сделать это вручную с помощью CSS здесь. Мы будем применять то же самое, но пусть директива сделает это за нас.

Для этого я написал в контроллер еще одну функцию.

function applyAnimation(element, props) {
  var duration, iterations, delay;
if (props.duration) {
    duration = {
      '-webkit-animation-duration': props.duration,
      '-moz-animation-duration': props.duration,
      '-o-animation-duration': props.duration,
      '-ms-animation-duration': props.duration
    }
  }
  if (props.delay) {
    delay = {
      '-webkit-animation-delay': props.delay,
      '-moz-animation-delay': props.delay,
      '-o-animation-delay': props.delay,
      '-ms-animation-delay': props.delay
    }
  }
  if (props.iterations) {
    iterations = {
      '-webkit-animation-iteration-count': props.iterations,
      '-moz-animation-iteration-count': props.iterations,
      '-o-animation-iteration-count': props.iterations,
      '-ms-animation-iteration-count': props.iterations
    }
  }
  if (props.disableOnSmallDevices === 'true') {
    element.addClass('disable-animated');
  }
  element.addClass('animated ' + props.animation);
  element.css(_.merge(duration, delay, iterations));
}

Когда disableOnSmallDevices имеет значение true, мы добавляем дополнительный класс с именем disable-animated (или как вам нравится). Этот класс отвечает за то, чтобы анимация не запускалась на небольших устройствах. Вы сами решаете, какое устройство считать небольшим. Мой класс disable-animated выглядит так:

.disable-animated { 
  @media only screen and (max-width : 990px) {
    /*CSS transitions*/
    -o-transition-property: none !important;
    -moz-transition-property: none !important;
    -ms-transition-property: none !important;
    -webkit-transition-property: none !important;
    transition-property: none !important;
    /*CSS transforms*/
    -o-transform: none !important;
    -moz-transform: none !important;
    -ms-transform: none !important;
    -webkit-transform: none !important;
    transform: none !important;
    /*CSS animations*/
    -webkit-animation: none !important;
    -moz-animation: none !important;
    -o-animation: none !important;
    -ms-animation: none !important;
    animation: none !important;
  }
}

Подобно addAnimations, мы создаем функцию removeAnimation:

function removeAnimation(element, props) {
  if (props.disableOnSmallDevices === 'true') {
    element.removeClass('disable-animated');
  }
  element.removeClass('animated ' + props.animation);
}

Теперь мы хотим реализовать функцию, которая будет назначать анимацию и запускать ее только тогда, когда элемент находится внутри области просмотра. Для этого мы используем путевые точки. Вы можете ознакомиться с их API здесь, но я также опишу, что я из него использовал.

Вот функция, отвечающая за добавление путевой точки к данному элементу:

function addWaypoint(element, props, expectedDirection) {
  var domElement = element.get(0);
return $timeout(function() {
    return new Waypoint({
      element: domElement,
      handler: function (scrollDirection) {
        if (
          props.triggerOnce === 'true' &&
          props.triggersCnt >= 1
        ) {
          return;
        }
        if (scrollDirection === expectedDirection) {
          applyAnimation(element, props);
          props.triggersCnt++;
        } else {
          removeAnimation(element, props);
        }
      },
      offset: props.triggerOffset
    });
  });
}

Все, что делает эта функция, - это вызов конструктора Waypoint с объектом JSON, содержащим следующее:

  • element - элемент DOM, на котором будет установлена ​​путевая точка.
  • обработчик - функция обратного вызова, которая будет выполняться, когда предметный элемент находится в области просмотра. Когда вызывается обработчик, он также вызывается с параметром, указывающим направление прокрутки (например, если вы прокрутите вниз до элемента, обработчик будет вызываться следующим образом: handler (‘down’)).
  • смещение - присоединяется к действию, когда элемент виден внутри области просмотра. Вы можете представить себе это свойство как воображаемую горизонтальную линию, пересекающую экран на определенной высоте, выраженной в процентах (%).

Обратите внимание, что эта функция использует $ timeout, поэтому вам нужно вставить его в директивную функцию, чтобы сделать ее доступной. Почему я использовал там $ timeout? Ответ: потому что на этапах инициализации приложения кажется, что в некоторых случаях элементы временно располагаются на странице выше, чем их обычное положение. Это происходит за доли секунды (я думаю, во время первого цикла дайджеста Angular), затем они быстро возвращаются на свои места. Хотя я не знаю наверняка, я думаю, что это может быть сильно связано со стилями CSS, используемыми для описания структуры страницы, и приоритетом шага привязки данных Angular. Если путевые точки установлены в самом начале, они обнаруживают, что некоторые элементы находятся в области просмотра, и сразу запускают анимацию (по ошибке) вместо того, чтобы применять такое же поведение только после того, как элемент находится в правильном положении на странице. Чтобы не рисковать срабатыванием триггера анимации раньше, чем следовало бы, я решил использовать $ timeout и откладывать каждый эффект addWaypoint до следующего цикла дайджеста (когда шаблон уже должен быть создан правильно).

При этом у нас есть все служебные функции, необходимые для записи функции ссылки. Вот окончательная структура контроллера animation. Детали каждой функции уже были показаны и обсуждались выше.

controller: function($scope) {
  $scope.utility = {
    applyAnimation: applyAnimation,
    removeAnimation: removeAnimation,
    addWaypoint: addWaypoint,
    getFinalProperties: getFinalProperties
  };
function getFinalProperties(attrs, animationsCtrl) {...}
  function applyAnimation(element, props) {...}
  function removeAnimation(element, props) {...}
  function addWaypoint(element, props, expectedDirection) {...}
}

А вот код функции ссылки animation:

link: function (scope, element, attrs, animationsCtrl) {
  var finalProps = scope.utility.getFinalProperties(attrs, animationsCtrl);
if (finalProps.trigger === 'scroll-down-to-element') {
    scope.utility.addWaypoint(element, finalProps, 'down');
  } else if (finalProps.trigger === 'scroll-up-to-element') {
    scope.utility.addWaypoint(element, finalProps, 'up');
  } else {
    scope.utility.applyAnimation(element, finalProps);
  }
}

Обратите внимание, что любое действие возобновляется вызовом API, который мы только что создали в контроллере:

  1. Сначала мы получаем свойства, которые необходимо присвоить.
  2. Если указано имя триггерного события, мы создаем и добавляем путевую точку к элементу.
  3. Если имя триггерного события отсутствует, мы просто добавляем класс анимации.

Вы можете увидеть, как эти директивы работают здесь.

Резюме

В этом руководстве я представил метод реализации директивы оболочки для стилей animate.css. Этот метод состоит из создания пары из двух директив, называемых анимация и анимация ( проверьте их в действии здесь). Первый предназначен для обертывания общих свойств анимации, а второй - для обозначения элементов, которые необходимо анимировать, и определения отдельных свойств, которые необходимо установить. Работа с этими директивами уменьшит дублирование кода в шаблоне, а также улучшит animate.css с возможностью запуска анимации только тогда, когда пользователь прокручивает анимированные элементы страницы. Кроме того, они устраняют необходимость вручную писать дополнительный CSS для настройки анимации по мере необходимости.

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

Здесь, в Algotech Solutions, мы любим писать о Javascript и Angular, а также о множестве других тем: хорошие методы кодирования, советы и хитрости Git, книжные рекомендации и облачные сервисы. Сообщите нам, какие ваши любимые темы, и оставайтесь на связи!

Первоначально опубликовано на www.algotech.solutions 21 апреля 2016 г. Подробнее о нашем стеке и чем мы делаем на algotech.solutions.

Если вам понравилась эта статья, пожалуйста, поделитесь ею с друзьями или щелкните маленькое зеленое сердечко ниже. Если вы хотите сначала получать наш последний контент, подпишитесь на нашу рассылку новостей. Без спама. Просто отличные инженерные посты!