AngularJS: animaciones con scroll

No tenía muy claro cómo titular esta entrada, dado que su título original es AngularJS: Scroll Animations. Como en ocasiones anteriores se trata de una “traducción-no-literal”/explicación de un artículo tutorial en inglés.

En fin, ¿a qué nos referimos con esto de animaciones con scroll? Seguro que habéis visto más de una página que las tiene. Se trata de animaciones que se activan conforme vas haciendo scroll en la página, al alcanzar una determinada posición. La web Let’s Free Congress es presentada con ejemplo de esto en el texto original.

AngularJS logo

La idea es que la animación comience cuando el usuario llegue a la parte de la pantalla, para lo cual nos serviremos de la directiva scrollPosition, que se nos presenta así en el ejemplo original

.directive('scrollPosition', ['$window', '$timeout', '$parse', function($window, $timeout, $parse) {
    return function(scope, element, attrs) {

        var windowEl = angular.element($window)[0];
        var directionMap = {
          "up": 1,
          "down": -1,
          "left": 1,
          "right": -1
        };

        // Recuperamos el elemento con el scroll
        scope.element = angular.element(element)[0];

        //Almacenamos los elementos que escuchan a este evento
        windowEl._elementsList = $window._elementsList || [];
        windowEl._elementsList.push({element: scope.element, scope: scope, attrs: attrs});

        var element, direction, index, model, scrollAnimationFunction, tmpYOffset = 0, tmpXOffset = 0;
        var userViewportOffset = 200;

        function triggerScrollFunctions() {

          for (var i = windowEl._elementsList.length - 1; i >= 0; i--) {
            element = windowEl._elementsList[i].element;
            if(!element.firedAnimation) {
              directionY = tmpYOffset - windowEl.pageYOffset > 0 ? "up" : "down";
              directionX = tmpXOffset - windowEl.pageXOffset > 0 ? "left" : "right";
              tmpXOffset = windowEl.pageXOffset;  
              tmpYOffset = windowEl.pageYOffset;  
              if(element.offsetTop - userViewportOffset < windowEl.pageYOffset && element.offsetHeight > (windowEl.pageYOffset - element.offsetTop)) {
                model = $parse(windowEl._elementsList[i].attrs.scrollAnimation)
                scrollAnimationFunction = model(windowEl._elementsList[i].scope)
                windowEl._elementsList[i].scope.$apply(function() {
                  element.firedAnimation = scrollAnimationFunction(directionMap[directionX]);  
                })
                if(element.firedAnimation) {
                  windowEl._elementsList.splice(i, 1);
                }
              }
            } else {
              index = windowEl._elementsList.indexOf(element); //TODO: Add indexOf polyfill for IE9 
              if(index > 0) windowEl._elementsList.splice(index, 1);
            }
          };
        };
        windowEl.onscroll = triggerScrollFunctions;
      };   
    }]);

Tal directiva se ha utilizado en este ejemplo. En ella podéis ver como la barra de la pantalla del móvil progresa según bajáis la barra.

En fin, la cosa es explicar ahora como usar esto. Como en el ejemplo original, iremos desgranando poco a poco el código, despedazándolo para ver cómo funciona.

Lo principal es añadir la directiva al elemento que queremos animar:

.row.show-for-large-up
  .teaser(scroll-position, scroll-animation='fireupApplicationDesignAnimation')
    .row
      .large-6.columns.left-align.margin-top
        h1(data-i18n="_CareerDesign_APPLICATIONDESIGNTITLE")

Esto le dice a AngularJS que cuando el usuario llegue al área determinada del DOM debe dispararse la animación. En vuestro controlador deberíais tener algo así

$scope.fireupApplicationDesignAnimation = function(scrollDirection) {
        scrollDirection > 0 ? reduceAmount() : aumentAmount(); // We want to increase on scrollDown
        setOffsetForImage();
    };

La directiva envía al controlador la dirección del scroll, lo que permite mostrar animaciones basadas en él. En ocasiones querrás ejecutar la animación sólo una vez en lugar de que ocurra cada vez que el scroll esté a su altura. Puedes lograrlo devolviendo un valor true y chequeándolo antes de lanzar la animación, como en este ejemplo:

$scope.fireupMarketingDesignAnimation = function() {
      if(!firedMarketingAnimation) {
        window.animations.marketingAnimation.init();
        firedMarketingAnimation = true;
        return firedMarketingAnimation;  
      }
    }

Como podéis ver en el ejemplo la función que lanza la animación está dentro de un if que hará que sólo se ejecute una vez.

En fin, resumiendo ¿Cómo funciona la directiva?. El proceso es el siguiente:

  • Crea un array de elementos que requieren un eventListener del tipo onScroll. Puedes tener varios almacenados, y si devuelves un true para una sola ejecución serán eliminados para reducir el uso de memoria.
  • Añade un eventListener onScroll que comprueba en qué parte de la página está el usuario.
  • Recorre tus elementos vinculados con la directiva comprobando si se han activado las animaciones.
  • Si no lo han hecho y el usuario se posiciona junto a ellas con el scroll entonces se activan. Esto obliga a hacer mucho uso de $parse, así que léete bien su documentación para entenderlo al 100%.
  • Si la animación llamada a través de scope devuelve true, la extrae del array, como dijimos en el primer punto.

La conclusión:

La directiva es útil cuando necesitas lanzar varias animaciones en un periodo de tiempo específico. También te permitirá definir un comportamiento específico según la posición del scroll o hasta para realizar cambios en propiedades CSS3 relacionándolas con la posición de la página.

En este ejemplo en Codepen, creado por el autor del artículo original, podéis ver cómo también funciona en horizontal. Si movéis el scroll lentamente veréis como el balón lo sigue. Eso sí, probad con Chrome porque a mi en Firefox en lugar de una pelota se me ve una especie de mojón raro.

En fin, finalmente recomendar el blog de jjperezaguinaga Surviving by Coding, donde podréis encontrar mucha info sobre AngularJS y otros temas de desarrollo web, en inglés.

AngularJS: Introducción y Hello World

Llevo unos días trasteando con el framework de Javascript AngularJS, libre y mantenido por Google. En su momento no lo incluí en mi pequeña selección de frameworks a ir mirando, pero al final he acabado por dedicarme a trastear con él. De momento tampoco he avanzado mucho, que este mes estoy liado, pero en el futuro os iré comentando.

Comencemos por la primera pregunta ¿para qué vale AngularJs? En fin, la idea de este framework es dinamizar las páginas HTML estáticas vinculando elementos de ese HTML con un modelo de datos que hemos definido en Javascript (a mano, o desde un JSON), permitiendo la interactividad con el usuario. En este caso, aunque se usa un patrón MVC la potencia es que nos permite, mediante bindings {{}} , acceder a la funcionalidad o a los datos. Es cierto que el resultado final lo podemos conseguir de otras muchas formas, pero AngularJS nos ahorrará muchas líneas de código. Algunos lo comparan ya con lo que significó jQuery en su momento para el desarrollo en Javascript.

AngularJS Logotipo
AngularJS

Para descargaros Angular, que por cierto cada versión tiene un nombre más delirante que la anterior (al momento de escribir esto estamos en la Monochromatic Rainbow, pero mi favorita es Flatulent Propulsion), podéis hacerlo desde la página del proyecto. Y desde la documentación de esa misma página he sacado el código para el primer ejemplo: un Hello World!. Pero claro, como hablamos de un framework que dota de actividad a vuestro CSS será más bien un Hello ______ (loquesea).

Empezamos, como siempre, por el marcado:


    <html ng-app>
      <head>
<script class="hiddenSpellError" type="text/javascript">// <![CDATA[
src</span>="http://code.angularjs.org/angular-1.0.2.min.js">
// ]]></script>
<script class="hiddenSpellError" type="text/javascript">// <![CDATA[
src</span>="script.js">
// ]]></script>
 </head>
 <body>
 <div ng-controller="HelloCntl">
 Your name: <input type="text" value="World" />
 <hr/>
 Hello {{name}}!
 <div>
 </body>
 </html>

Bien, como puedes ver en la cabecera cargamos como un script la librería de AngularJS (está copiado de un ejemplo antiguo, así que revisa cual es la última versión en el momento en que lo hagas) y debajo cargamos otro script con lo que hayamos programado en Javascript. Puedes ver que al div le hemos asignado un atributo llamado ng-controller. La idea de esto es definir que ese div estará dentro del ámbito del controlador que le indicamos (recuerda, estamos trabajando en un modelo MVC). Luego verás que al input le hemos puesto también un atributo, en ese caso ng-model. Lo mismo, estamos vinculando ese atributo al ámbito del modelo de datos definido, o explicado de forma más simple: el texto que hay ahí será el modelo de datos y se llamará name. Finalmente ves que al lado de Hello hemos incluido {{name}}, esencialmente la idea es que ese elemento entre llaves está relacionado en el elemento del modelo con el mismo nombre (que a su vez está relacionado con el input) ¿qué va a ocurrir? Pues el llamado two-way-data-binding: modificarás el modelo desde la vista y justo al mismo tiempo verás como los datos de la vista se modifican.

Ahora vamos con la parte del javascript, donde definimos el controlador y el modelo:

    function HelloCntl($scope) {
       $scope.name = 'World';
    }

Como puedes ver aquí definimos la función HelloCntl, que fue la que vinculamos en el marcado al div. Abajo simplemente hacemos que, por defecto, el nombre que aparezca sea World (para formar el Hello World). Si bien, si lo modificamos veremos como este cambia.

Logo AngularJS

Finalmente vamos a hablar de otro de los fuertes de AngularJS: el testeo del código. Porque AngularJS nos permite escribir test para comprobar el correcto funcionamiento, sin tener que meternos en absurdas y largas pruebas con algún complemento del navegador para depurar javascript. El teste sería este:

    it('should change the binding when user enters text', function() {
        expect(binding('name')).toEqual('World');
        input('name').enter('angular');
        expect(binding('name')).toEqual('angular');
    });

Si queréis ver en acción todo el ejemplo antes de testearlo vosotros, en la página de Angular (de donde lo he copiado yo) podréis hacerlo. Además de ver otros, como un juego del tres en raya.

En fin, ya puedes hacerte una idea de la potencia de este AngularJS, más adelante espero escribir más sobre el tema. Yo lo veo como una potente herramienta, sobre todo para el desarrollo de Apps móviles con HTML5 y CSS.

También añadiré que la principal idea es usar AngularJS con Node.js en el lado del servidor, si bien podéis desde ese lado el lenguaje que prefiráis (Ruby, PHP, Python).