En este post vamos a presentar una pequeña pero muy útil directiva para Angular. La idea será poder editar un elemento HTML (en este caso, una etiqueta span) al hacer doble click sobre dicho elemento. Las directivas de Angular permiten extender (o decorar) elementos HTML con comportamientos, y manipular el DOM de manera relativamente sencilla pero muy potente, y además elegante y reutilizable.
La inspiración para este post vino a raíz de que hace unos días, en nuestro post de unos meses atrás Listas anidadas en AngularJS 1.2 en el que incluimos una pequeña demo de una ToDo app, recibimos la siguiente pregunta:
“Y si quisiera corregir o actualizar el nombre de una tarea que tendría que hacer?”
Muy buena pregunta, sí señor! Es una funcionalidad tan básica que se nos debería haber ocurrido antes… Además, lo bueno es que gracias al poder de las directivas de Angular, podemos programar esa funcionalidad de manera totalmente modular y con cero acoplamiento de código. Creo que este es el primer post que escribimos basado en una request en uno de vuestros comentarios, pero la verdad es que es una cuestión muy interesante, y ya hacía tiempo que queríamos escribir un post dedicado a las absolutamente indispensables directivas de AngularJS.
Veamos primero la demo actualizada, con la funcionalidad de hacer doble click sobre los nombres de las tareas para editar su nombre inline, además de algunos pequeños detalles de usabilidad como poder finalizar el «modo edición» presionando ESC o Enter:
http://www.lostiemposcambian.com/blog/posts/angular-nested-lists/
La única diferencia con respecto a la demo anterior ha sido la incorporación de la directiva ltc-editable (ltc por Los Tiempos Cambian, of course). Veamos el código fuente de dicha directiva, comentando para mayor claridad:
app.directive("ltcEditable", function($document) { return { scope: { text: "=ngModel" }, restrict: 'A', // restrict directive to use as attribute link: function (scope, element, attrs) { // restrict this directive to span elements if(element[0].nodeName !== 'SPAN') { return; } // store refererences var $ = angular.element; var body = $($document[0].body); var input = $('<input type="text"/>'); // initial value element.text(scope.text); // shit happens on double click on element element.on("dblclick", function() { // class name classOff will prevent editable behaviour if element has that class if(!element.hasClass(attrs['classOff'])) { // swap span for input input.val(element.text()); element.parent().append(input); input[0].focus(); element.text(''); // hide element (sort of!) // nice ux 🙂 input.on("keydown", function(event) { if(event.which == 13 || event.which == 27) { input.triggerHandler('blur'); } }); // handle click outside body.on('click', function(event) { if(event.target !== input[0]) { input.triggerHandler('blur'); } }); // on blur, store value and clean up after ourselves input.on('blur', function(event) { // unregister listeners! body.off(); input.off(); // set value - could validate before! element.text(input.val()); // clean up input.remove(); }); } }); } }; });
Un detalle que pensamos es importante es que NO estamos utilizando la ubicua librería jQuery en este ejemplo, ya que para la (poca) manipulación de elementos del DOM nos basta y nos sobra con el pequeño wrapper jQueryLite de Angular y las funciones nativas de JavaScript. No incluir jQuery a no ser que sea estrictamente necesario es una buena práctica que intentamos seguir a rajatabla!
Y por último, para utilizar esta directiva en nuestro template, tan sencillo como aplicarla como atributo de una etiqueta span:
... <!-- cambiamos: <span ng-class="{done:item.completed}">{{item.name}}</span> por: --> <span ng-class="{done:item.completed}" ltc-editable class-off="done" ng-model="item.name"> </span> ...
Un ejercicio tal y como más nos gusta, sencillo pero funcional y al grano, y además con mucho margen de mejora… Por ejemplo, cómo haríamos que esta directiva se pudiera aplicar también a tags HTML DIV, y en ese caso tuviésemos que usar una textarea en vez de un input? Lo dejamos aquí planteado para nuestros lectores! 😉
no funaciona me marca un error
angular.js:7889 XMLHttpRequest cannot load file:///home/net/Desktop/toDo/angular-nested-lists/app/templates/list.html. Cross origin requests are only supported for protocol schemes: http, data, chrome-extension, https, chrome-extension-resource.
angular.js:9159 Error: Failed to execute ‘send’ on ‘XMLHttpRequest’: Failed to load ‘file:///home/net/Desktop/toDo/angular-nested-lists/app/templates/list.html’.
at Error (native)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:7889:11
at sendReq (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:7720:9)
at serverRequest (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:7454:16)
at wrappedCallback (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:10655:81)
at wrappedCallback (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:10655:81)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:10741:26
at Scope.$eval (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:11634:28)
at Scope.$digest (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:11479:31)
at Scope.$apply (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.js:11740:24)
3 años después, me salvan la vida.