miércoles, 25 de enero de 2017

MetisMenu con AngularJS

Hoy analizaremos las oportunidades que nos brinda MetisMenu cuando lo combinamos con AngularJS.

Pero antes…

¿Qué es MetisMenu?
Es un plugin escrito en jQuery diseñado para armar menús animados con gran facilidad.

¿Qué es AngularJS?
Es un framework de código abierto del tipo MVC (Modelo Vista Controlador) de Javascript para el desarrollo Web Front End que permite crear aplicaciones del tipo SPA (Single-Page Applications). Su desarrollo es realizado por Google.

Entonces, ¿cuáles son los beneficios de combinar ambas tecnologías? 

Podríamos pensar en armar un menú cuyos elementos surjan de la iteración brindada por AngularJS de elementos de un array.

Veamos un ejemplo.

 

Ejemplo de metisMenu

Este es el esquema inicial de nuestra página web:

<html>
                <head>
                               <meta charset="UTF-8">
                               <Title>metisMenu</Title>
                </head>  
                <body>
                </body>
</html>

Lo primero será entonces agregar las referencias a las librerías de nuestro plugin. En este ejemplo vamos a utilizar los CDN (Content Delivery Network), que básicamente son sitios permanentemente online que disponibilizan dichas librerías gratuitamente. Otra opción es bajar los archivos desde la página GitHub oficial del plugin y referenciarlos localmente.

<head>
                <meta charset="UTF-8">
                <Title>metisMenu</Title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.css">
                <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
                <script src="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.js"></script>
</head>

Acto seguido vamos a crear nuestro menú en el body:

<body>
                <ul class="metismenu" id="menu">
                               <li class="active">
                                               <a href="#" aria-expanded="true">Menu 1</a>
                                               <ul aria-expanded="true">
                                                               <li><a href="#">item 1.1</a></li>
                                                               <li><a href="#">item 1.2</a></li>
                                                               <li><a href="#">item 1.3</a></li>
                                                               <li><a href="#">item 1.4</a></li>
                                               </ul>
                               </li>
                               <li>
                                               <a href="#" aria-expanded="false">Menu 2</a>
                                               <ul aria-expanded="false">
                                                               <li><a href="#">item 2.1</a>
                                                                              <ul aria-expanded="false">
                                                                                              <li><a href="#">item 2.1.1</a></li>
                                                                                              <li><a href="#">item 2.1.2</a></li>
                                                                                              <li><a href="#">item 2.1.3</a></li>
                                                                                              <li><a href="#">item 2.1.4</a></li>
                                                                              </ul>
                                                               </li>                                                                                                     
                                                               <li><a href="#">item 2.2</a></li>
                                                               <li><a href="#">item 2.3</a></li>
                                                               <li><a href="#">item 2.4</a></li>
                                               </ul>
                               </li>         
                </ul>
</body>

Y por último, justo antes del cierre de la etiqueta body, agregamos una llamada a la función metisMenu() que habilita finalmente la animación de nuestro menu:

<body>
                <script type="text/javascript">
                               $(document).ready(function(){                                              
                                               $("#menu").metisMenu();
                               });
                </script>
</body>

Haga clic aquí para ver este ejemplo en vivo.



Con unos retoques podemos agregarle más color y distintas animaciones a nuestro menú, pero ese no es el objetivo de este artículo. Para más ejemplos se recomienda revisar los enlaces al final.

 

Ejemplo de metisMenu sumando AngularJS

A continuación, agregaremos AngularJS a nuestro código. La idea es la siguiente: vamos a simular un repositorio de datos el cual será leído mediante una llamada Ajax (asincrónica) y con esa información se estará creando nuestro menú.Por esa razón vamos a estar reemplazando el código que contiene nuestro menú estático (Menú 1; ítem 1.1, etc.) por código de AngularJS. Recordemos que en este caso será AngularJS quien se encargue de armar los elementos HTML con la información que va a leer de nuestras funciones Ajax.

Agregamos el link al código de AngularJS en nuestro head:
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
Nuestro head debería quedar así:
<head>
                <meta charset="UTF-8">
                <Title>metisMenu</Title>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.css">
                <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
                <script src="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.js"></script>
                <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
</head>

Lo siguiente será modificar nuestro body para incorporar el código de AngularJS.,Nuestra aplicación se llamará 'miAplicacion' y definiremos un sólo controlador: 'miControlador':

<body ng-app="miAplicacion" ng-controller="miControlador">

Definimos una lista desordenada, tal cual como en el ejemplo anterior de metisMenu:

                <ul class="metismenu" id="menu">

Sin embargo, ahora cada elemento de la lista se va a estar generando mediante la instrucción 'ng-repeat', que va a iterar cada país de una lista y vamos a estar monitoreando el final de dicha iteración. Más adelante vamos a entender el porqué de esta acción.

<li ng-repeat="pais in paises" ng-repeat-end-watch="miFuncion">
<a href="#" >{{ pais.Country }}</a>
                <ul >

Aquí vamos a estar definiendo los elementos internos o submenús de nuestra lista y como queremos que siga siendo dinámica.Para ello, volvemos a utilizar la instrucción ng-repeat de AngularJS:

        <li ng-repeat="nombre in nombres" ng-repeat-end-watch="miFuncion">
                                                               <a href="#" >{{ nombre.Name }}</a>
                                               </li>
                                </ul>
</li>
</ul>



Lo próximo será definir nuestro código javascript, el cual recomiendo que sea puesto en un archivo aparte para mantener ordenado nuestro código. Aquí lo definiremos en el mismo código HTML para fines educativos y de simplicidad. Dicho código va al final del código que va dentro de las etiquetas body.

Comenzamos definiendo un módulo de Angular que va a tener la directiva ngRepeatEndWatch.

Y aquí surge una cuestión, cuando armamos un menú del tipo metisMenu, luego de definir los elementos debemos llamar al método (#idDelElementoContenedor).metisMenu(). Dicha llamada sólo puede realizarse cuando todos los elementos del menú están definidos.El problema con AngularJS es que justamente son llamadas Ajax dinámicas y asincrónicas, entonces tenemos que ubicar la llamada a metisMenu()cuando se terminan de obtener los datos a presentar y se arman los nuevos elementos HTML. Justo allí hay que ubicar la llamada a metisMenu y de esa manera nuestro menú se va a presentar correctamente, es decir, con la funcionalidad de colapsado y des colapsado. Para ello, necesitaremos definir una directiva que esté 'monitoreando' el fin del ng-repeat:

angular.module('miModulo', []).directive('ngRepeatEndWatch', function () {
                return {
                               restrict: 'A',
                               scope: {},
                               link: function (scope, element, attrs) {
                                               if (attrs.ngRepeat) {
                                                               if (scope.$parent.$last) {
                                                                              if (attrs.ngRepeatEndWatch !== '') {
                                                                                              if (typeof scope.$parent.$parent[attrs.ngRepeatEndWatch] === 'function') {
                                                                                                              scope.$parent.$parent[attrs.ngRepeatEndWatch]();
                                                                                              } else {
                                                                                                              scope.$parent.$parent[attrs.ngRepeatEndWatch] = true;
                                                                                              }
                                                                              } else {
                                                                                              scope.$parent.$parent.ngRepeatEnd = true;
                                                                              }
                                                               }
                                               } else {
                                                               throw 'falta ngRepeatEndWatch';
                                               }
                               }
                };
});


Una vez establecida la directiva, definimos nuestra aplicación 'miAplicacion' de AngularJS, quien va a contener 'miModulo' que a su vez es quien monitorea el fin del ng-repeat:
var miAplicacion = angular.module('miAplicacion', ['miModulo']);
Definimos un controller y dentro del mismo estaremos definiendo la variable 'miFuncion' que es el nombre que pusimos en el codigo html: ng-repeat-end-watch="miFuncion"

miAplicacion.controller('miControlador', function ($scope, $http) {
                $scope.miFuncion = function () {
                               $("#menu").metisMenu();
                }

Por último, agregamos dos llamadas a un repositorio de información:

                $http.get("http://www.w3schools.com/angular/customers_sql.aspx")
                .then(function (response) {$scope.paises = response.data.records;});

                $http.get("http://www.w3schools.com/angular/customers_mysql.php")
                .then(function (response) {$scope.nombres = response.data.records;});

                });
</body>

A continuación, se presenta el código completo. Se puede copiar y pegar en cualquier editor de texto y guardarlo como ".HTML":

<html>
                <head>
                               <meta charset="UTF-8">
                               <Title>metisMenu</Title>
                               <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.css">
                               <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
                               <script src="https://cdnjs.cloudflare.com/ajax/libs/metisMenu/2.5.2/metisMenu.min.js"></script>
                               <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
                </head>
                <body ng-app="miAplicacion" ng-controller="miControlador">
                               <ul class="metismenu" id="menu">
                                               <li ng-repeat="pais in paises" ng-repeat-end-watch="miFuncion">
                                                               <a href="#" >{{ pais.Country }}</a>
                                                               <ul >
                                                                              <li ng-repeat="nombre in nombres">
                                                                                              <a href="#" >{{ nombre.Name }}</a>
                                                                              </li>
                                                               </ul>
                                               </li>
                               </ul>
                               <script type="text/javascript">
                                               angular.module('miModulo', []).directive('ngRepeatEndWatch', function () {
                                                               return {
                                                                              restrict: 'A',
                                                                              scope: {},
                                                                              link: function (scope, element, attrs) {
                                                                                              if (attrs.ngRepeat) {
                                                                                                              if (scope.$parent.$last) {
                                                                                                                             if (attrs.ngRepeatEndWatch !== '') {
                                                                                                                                             if (typeof scope.$parent.$parent[attrs.ngRepeatEndWatch] === 'function') {
                                                                                                                                                             scope.$parent.$parent[attrs.ngRepeatEndWatch]();
                                                                                                                                             } else {
                                                                                                                                                             scope.$parent.$parent[attrs.ngRepeatEndWatch] = true;
                                                                                                                                             }
                                                                                                                             } else {
                                                                                                                                             scope.$parent.$parent.ngRepeatEnd = true;
                                                                                                                             }
                                                                                                              }
                                                                                              } else {
                                                                                                              throw 'falta ngRepeatEndWatch';
                                                                                              }
                                                                              }
                                                               };
                                               });

                                               var miAplicacion = angular.module('miAplicacion', ['miModulo']);

                                               miAplicacion.controller('miControlador', function ($scope, $http) {
                                                               $scope.miFuncion = function () {
                                                                              $("#menu").metisMenu();
                                                               }

                                                               $http.get("http://www.w3schools.com/angular/customers_sql.aspx")
                                                               .then(function (response) {$scope.paises = response.data.records;});

                                                               $http.get("http://www.w3schools.com/angular/customers_mysql.php")
                                                               .then(function (response) {$scope.nombres = response.data.records;});

                                               });
                               </script>
                </body>
</html>

 

Enlaces útiles



Autor:



Julián Haeberli

Baufest Technical Expert