« Back
read.
Cómo "pensar en AngularJS" si vienes del mundo JQuery

Cómo "pensar en AngularJS" si vienes del mundo JQuery.

1. No diseñes tu web para luego cambiarla manipulando el DOM

En JQuery1, primero diseñas una página, y luego la haces dinámica. Eso es porque JQuery fue diseñado para potenciar el HTML y ha crecido enormemente desde aquella simple premisa.

Pero en AngularJS, tienes que empezar desde el principio teniendo tu arquitectura en mente. En vez de empezar pensando "Tengo este trozo de DOM y quiero que haga X", tienes que empezar pensando en lo que de verdad quieres conseguir, entonces es cuando diseñas tu aplicación y finalmente, es cuando diseñas la vista.

2. No potencies JQuery usando AngularJS

De manera similar, no empieces con la idea de que JQuery hace X, Y y Z, así que simplemente añado AngularJS por encima de todo eso para tener modelos y controladores. Eso es realmente tentador cuando estás empezando, he ahí por qué siempre recomiendo a nuevos programadores de AngularJS que no usen JQuery en absoluto, por lo menos hasta que se acostumbren a hacer las cosas al estilo de AngularJS (aka: the "Angular Way").

He visto a muchos desarrolladores (en StackOverflow) crear esas elaboradas soluciones con plugins de JQuery de 150 a 200 líneas de código que luego meten dentro de AngularJS con una colección de callbacks y $applys que son confusos y enrevesados; pero al final consiguen que funcione! El problema es que en la mayoría de los casos un plugin de JQuery puede ser reescrito en AngularJS en una fracción del código, donde de repente se vuelve claro y comprensible.

El razonamiento base es este: cuando busques soluciones, primero "piensa en AngularJS"; si no puedes hacerlo, pregunta a la comunidad; si después de todo eso sigue sin haber solución, entonces siéntete libre de usar JQuery. Pero no uses JQuery como el camino fácil o nunca dominarás AngularJS.

3. Piensa siempre en términos de la arquitectura

Primero has de saber que las single-page applications son precisamente eso, aplicaciones. No son páginas web. Así que tenemos que pensar como un Back-End Developer además de pensar como un Front-End Developer. Tenemos que pensar en cómo dividir nuestra aplicación en componentes individuales, extensibles, fáciles de testear.

Bien y cómo haces eso? Cómo puedes "pensar en AngularJS"? He aquí algunos principios generales, contrastados con JQuery.

La vista es el "registro oficial"

En JQuery, programáticamente modificamos la vista. Podríamos tener un menú dropdown definido como un ul tal que así:

    <ul class="main-menu">
        <li class="active">
            <a href="#/home">Home</a>
        </li>
        <li>
            <a href="#/menu1">Menu 1</a>
            <ul>
                <li><a href="#/sm1">Submenu 1</a></li>
                <li><a href="#/sm2">Submenu 2</a></li>
                <li><a href="#/sm3">Submenu 3</a></li>
            </ul>
        </li>
        <li>
            <a href="#/home">Menu 2</a>
        </li>
    </ul>

En JQuery, en la lógica de nuestra aplicación, lo activaríamos con algo como esto:

    $('.main-menu').dropdownMenu();

Cuando miramos la vista, no es inmediatamente obvio que haya cierta funcionalidad. Para pequeñas aplicaciones, está bien. Para aplicaciones sustanciales, las cosas se complican rápidamente y se hacen difíciles de mantener.

En AngularJS, sin embargo, la vista es el registro oficial de la funcionalidad de la vista. Nuestro menú ul se podría declarar así:

    <ul class="main-menu" dropdown-menu>
        ...
    </ul>

Estos dos ejemplos hacen lo mismo, pero en la versión de AngularJS cualquiera que mire el template sabe qué debe hacer. Cada vez que un nuevo miembro del equipo de desarrollo llegue abordo, puede mirar esto y entonces saber con certeza que hay una directiva llamada dropdownMenu operando; no necesita intuir la respuesta correcta o irse a buscar el código. La vista nos dice qué se supone que debe hacer. Mucho más claro.

Desarrolladores nuevos en AngularJS suelen preguntar cosas como: cómo puedo encontrar todos los enlaces de un tipo determinado y añadirles una directiva. El programador siempre se queda atónito cuando le respondemos: no lo hagas. Pero la razón por la que no debes hacerlo es porque sería mezclar JQuery con AngularJS, nada bueno en absoluto. El problema aquí es que el programador está intentando "hacer JQuery" en el contexto de AngularJS. Eso nunca va a funcionar bien. La vista es el registro oficial. Fuera de una directiva (más sobre esto abajo) nunca, jamás, jamás debes cambiar el DOM. Y las directivas se aplican en la vista, así que el propósito está claro.

Recuerda: no diseñes, y luego piques el código. Haz la arquitectura, y luego el diseño.

Data binding

Esta es de lejos una de las mejores funcionalidades de AngularJS y que evita a su vez la clase de manipulaciones del DOM de las que hablaba en la sección anterior. AngularJS actualizará automáticamente la vista para que tú no tengas que hacerlo! En JQuery, respondemos a eventos para luego actualizar el contenido. Algo como esto:

    $.ajax({
      url: '/myEndpoint.json',
      success: function ( data, status ) {
        $('ul#log').append('<li>Data Received!</li>');
      }
    });

Para una vista que tiene esta pinta:

    <ul class="messages" id="log"></ul>

Aparte de mezclar conceptos, también tenemos el problema del propósito del que hablaba antes. Pero más importante aún, tenemos que manualmente referenciar y actualizar un nodo del DOM. Y si quisiéramos eliminar una entrada en el log, tendríamos que atacar el DOM de nuevo para hacer eso también. ¿Cómo testeamos la lógica aparte del DOM? ¿Y si queremos cambiar la presentación?

Esto es un poco ñapa, pero en AngularJS, podríamos hacer esto:

    $http('/myEndpoint.json').then(function (response) {
        $scope.log.push({ msg: 'Data Received!' });
    });

Y nuestra vista se vería así:

    <ul class="messages">
        <li ng-repeat="entry in log">{{ entry.msg }}</li>
    </ul>

Pero para este propósito, lo haremos así:

    <div class="messages">
        <div class="alert" ng-repeat="entry in log">
            {{ entry.msg }}
        </div>
    </div>

Y ahora en lugar de una lista desordenada, estamos usando cajas de alert de Bootstrap. Y ni siquiera hemos tenido que tocar el código del controlador! Peor más importante aún, no importa el dónde ni el cómo se actualice el log, la vista cambiará también, automáticamente. Limpio!

Aunque no lo he enseñado aquí, el data binding es de dos caminos. Así que esos mensajes del log podrían ser editados en la vista simplemente haciendo esto: <input ng-model="entry.msg" />. Y entonces hubo un regocijarse.

Modelo separado en otra capa

En JQuery, el DOM es algo así como el modelo. Pero en AngularJS, tenemos una capa separada para el modelo que podemos gestionar como queramos, completamente independiente de la vista. Esto ayuda para el ya mencionado data binding, mantiene la separación de conceptos, e introduce muchísima más facilidad de testear.

Separación de conceptos

Y todo lo anterior se resume en este principio universal: mantén los conceptos separados. Tus vistas son el registro oficial de lo que se debe hacer (la mayor parte); el modelo representa tus datos; tienes una capa de servicios para realizar tareas reutilizables; realizas las operaciones sobre el DOM y potencias la vista con directivas; y lo mantienes todo junto con los controladores. Sólo me queda hablar sobre los tests, cosa que haré en otra sección más abajo.

Inyección de dependencias

Para ayudarnos con la separación de conceptos está la inyección de dependencias (aka: Dependency Injection, en adelante, DI). Si vienes de un lenguaje de entorno servidor (desde Java hasta PHP) probablemente este concepto te sea familiar, pero si eres un Front-End que viene del mundo JQuery, este concepto puede que te parezca algo desde superfluo pasando por estúpido hasta hipster. Pero no lo es. :-)

Desde una perspectiva muy amplia, la DI significa que puedes declarar componentes libremente y luego desde otros componentes, simplemente pedir una instancia de ellos y te será concedida. No tienes por qué saber nada sobre el orden en el que se carga, la localización de los archivos ni nada de eso. Su potencial pude no ser visible inmediatamente, pero pondré sólo un ejemplo (común): el testing.

Digamos que en nuestra aplicación, necesitamos un servicio que implemente almacenamiento en un servidor a través de una API REST y que, dependiendo del estado de la aplicación, también tenga almacenamiento local (local storage). Cuando corramos los tests en el controlador, no queremos que se comunique con el servidor - estamos testeando el controlador, después de todo. Simplemente podemos añadir un mock del servicio con el mismo nombre que el componente original, y el inyector se asegurará de que nuestro controlador obtenga el falso automáticamente - nuestro controlador no sabe y no debe saber la diferencia.

Hablando de testing...

4. Desarrollo orientado a tests - siempre

En realidad eso es parte de la sección 3 sobre arquitectura, pero es tan importante que la estoy poniendo en su propia sección.

De todos los muchísimos plugins de JQuery que hayas visto, usado o escrito, ¿cuántos venían acompañados de una buena suite de tests? No muchos porque JQuery no se presta demasiado a ello, pero AngularJS sí.

En JQuery, la única manera de testear es la mayoría de las veces crear un componente separado con un ejemplo/demo contra la cual nuestros tests puedan realizar manipulaciones en el DOM. Así que debemos desarrollar un componente por separado y entonces integrarlo en nuestra aplicación. Qué inconveniente! La mayoría del tiempo, cuando desarrollamos con JQuery, optamos por un desarrollo iterativo en vez de uno orientado a tests. Y quién podría culparnos?

Pero precisamente porque tenemos separación de conceptos, podemos hacer desarrollo orientado a tests iterativamente en AngularJS! Por ejemplo, supongamos que queremos hacer una directiva super simple para indicar en nuestro menú cuál es nuestra ruta actual. Podemos declarar lo que queremos en la vista de nuestra aplicación:

    <a href="/hello" when-active>Hello</a>

Ok, y ahora podemos escribir los tests para nuestra no existente, directiva when-active:

    it('should add "active" when the route changes', inject(function() {
        var elm = $compile('<a href="/hello" when-active>Hello</a>')($scope);

        $location.path('/not-matching');
        expect(elm.hasClass('active')).toBeFalsey();

        $location.path('/hello');
        expect(elm.hasClass('active')).toBeTruthy();
    }));

Y cuando corremos los tests, obviamente confirmamos que fallan. Ahora es el momento de crear nuestra directiva:

    .directive('whenActive', function ($location) {
        return {
            scope: true,
            link: function (scope, element, attrs) {
                scope.$on('$routeChangeSuccess', function () {
                    if ($location.path() == element.attr('href')) {
                        element.addClass('active');
                    }
                    else {
                        element.removeClass('active');
                    }
                });
            }
        };
    });

Nuestros tests ahora pasan y nuestro menú se comporta como debe. Nuestro desarrollo es iterativo a la par que orientado a tests. Brutalmente genial.

5. Conceptualmente, las directivas no son JQuery encapsulado

Con frecuencia oirás la frase "manipula el DOM sólo dentro de las directivas". Esto es absolutamente necesario. Hazlo con el respeto que se merece!

Pero ahondemos un poco más...

Algunas directivas simplemente decoran lo que ya está en la vista (piensa en ngClass) y por lo tanto a veces manipulan el DOM directamente y básicamente ya está. Pero si una directiva es como un "widget" y tiene un template, también debería respetar la separación de conceptos. Esto implica que el template también debería permanecer mayoritariamente independiente de su implementación en las funciones link y del controlador.

AngularJS viene con un completo set de herramientas para hacer esto muy sencillo; con ngClass podemos cambiar dinámicamente la clase; ngBind permite hacer two-way data binding; ngShow y ngHide programáticamente muestran u ocultan un elemento; y muchas otras - incluidas las que podemos escribir nosotros mismos. En otras palabras, podemos hacer esta clase de cosas increíbles sin manipular el DOM. Cuanto menos toquemos el DOM, más fáciles de testear serán nuestras directivas, más fácil añadirles estilos, más fáciles de cambiar en el futuro, y más reusables y distribuidas serán.

He visto a muchos programadores novatos en AngularJS usar directivas como su sitio para escupir un montón de JQuery. En otras palabras, piensan "como no puedo manipular el DOM en el controlador, pondré ese código dentro de una directiva". Mientras que eso es ciertamente mejor, simplemente sigue estando mal.

Piensa en el log que programamos en la sección 3. Incluso si pusiéramos ese código en una directiva, seguimos queriendo hacerlo "the Angular Way". Se puede seguir haciendo sin manipular el DOM! Hay muchas ocasiones en las que esto es necesario, pero son mucho menos frecuentes de lo que puedas pensar! Antes de tocar el DOM en cualquier parte de tu aplicación, pregúntate a ti mismo si de verdad necesitas hacerlo. Debe de haber una forma mejor.

Aquí dejo un pequeño ejemplo que demuestra el patrón que veo con más frecuencia. Queremos un botón toggleable. (Nota: este ejemplo es un poco forzado y farragoso para representar casos más complicados que se resuelven exactamente de la misma manera.)

    .directive( 'myDirective', function () {
        return {
            template: '<a class="btn">Toggle me!</a>',
            link: function ( scope, element, attrs ) {
                var on = false;

                $(element).click( function () {
                    on = !on;
                    $(element).toggleClass('active', on);
                });
            }
        };
    });

Hay algunas cosas que están mal:

  1. Primero, JQuery no era necesario. No hemos hecho nada que requiera JQuery en ningún momento!
  2. Segundo, incluso si ya teníamos JQuery en la página, no hay razón para utilizarlo aquí; simplemente podemos usar angular.element y nuestro componente seguirá funcionando cuando lo incorporemos en otro proyecto que no tenga JQuery.
  3. Tercero, incluso asumiendo que JQuery era necesario para que funcionase esta directiva, jqLite (angular.element) siempre usará JQuery si estaba cargado! Así que no necesitamos usar el $ - simplemente podemos usar angular.element.
  4. Cuarto, estrechamente relacionado con el tercero, es que los elementos de jqLite no necesitan estar envueltos en el $ - el element que se pasa a la función link ya será de antemano un elemento JQuery!
  5. Y quinto, que ya hemos mencionado en secciones anteriores, ¿por qué estamos mezclando cosas del template dentro de nuestra lógica?

La directiva puede res reescrita (incluso para casos muy complicados!) de manera mucho más simple:

    .directive( 'myDirective', function () {
        return {
            scope: true,
            template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
            link: function ( scope, element, attrs ) {
                scope.on = false;

                scope.toggle = function () {
                    scope.on = !scope.on;
                };
            }
        };
    });

De nuevo, las cosas del template están en el template, así que tú (o tus usuarios) puedes fácilmente cambiar el estilo cuando sea necesario, y la lógica permanecerá intacta. Reusabilidad - boom!

Y aún están ahí todos esos otros beneficios, como el testeo - es fácil! No importa el template, la API interna de las directivas nunca se toca, así que refactorizar es muy sencillo. Puedes cambiar el template tanto como quieras sin tocar la directiva. Y no importa lo que cambies, tus tests seguirán pasando.

QUÉEE!?

Entonces si las directivas no son colecciones de funciones parecidas a JQuery, qué son? Las directivas son en realidad extensiones del HTML. Si el HTML no hace algo que tú necesitas que haga, escribes una directiva que lo haga por ti, y entonces la usa como si ya formara parte del HTML.

Dicho de otro modo, si AngularJS no hace algo de serie, piensa en cómo conseguir que el equipo de desarrollo lo encaje usando ngClick, ngClass, etc.

Conclusión

No utilices JQuery. Ni siquiera lo incluyas. Mermará tu progreso. Cuando estés frente a un problema que pienses que podrías resolver con JQuery, antes de poner el $, intenta pensar una manera de hacerlo dentro de los confines de AngularJS. Y si no sabes hacerlo, pregunta! 19 veces de 20, la mejor manera de hacerlo no requiere utilizar JQuery e intentar resolverlo con JQuery resulta en mucho más trabajo para ti.


  1. El artículo original en inglés es una respuesta muy famosa en StackOverflow

comments powered by Disqus