En tota aplicació web, normalment el seu conjunt de dades creix amb el temps i és necessari buscar la informació sobre la que volem treballar. Per fer-ho es restringeixen les dades aplicant un criteri. Un criteri pot ser peticions d'un municipi, factures d'un client, en un interval de temps, un número d'expedient...
Per aquesta raó a la part superior del grid que mostra tots els registres acostumo a posar un filtre, que permet introduir els valors del criteri que es vol fer servir per restringir els registres del grid. El HTML corresponent a aquest formulari de filtrat és molt senzill. Aleshores l'única part que revesteix alguna dificultat és recollir els paràmetres i enviar-los al ASHX corresponent. El ASHX invocarà al LoadGrid, clsDades QUERY i finalment a la stored procedure QUERY.
Per recollir els paràmetres faig servir una funció Javascript Filtrar (nom molt original). Aquesta funció un cop té els valors dels criteris els posa a la URL que carrega el grid i invoca al mètode del grid trigger reloadGrid. Això últim fa que el grid actualitzi els seus valors amb els nous criteris aplicats. Com sempre el codi:
function Filtrar() {
var FK_Arxiu = $("#frmFiltre #FK_Arxiu").val();
var FK_LlocDiposit = $("#frmFiltre #FK_LlocDiposit").val();
var SigTop = $("#frmFiltre #SigTop").val();
var Capsa = $("#frmFiltre #Capsa").val();
var Inutilitzada = $("input[name=lInutilitzada]:checked").val();
var FK_CapsaNULL = $("input[name=lFK_CapsaNULL]:checked").val();
var URL = 'sigtop.ashx?action=loadgrid&FK_Arxiu=' + FK_Arxiu + '&Capsa=' + Capsa;
URL += '&FK_LlocDiposit=' + FK_LlocDiposit + '&SigTop=' + SigTop;
URL += '&lInutilitzada=' + Inutilitzada + '&lFK_CapsaNULL=' + FK_CapsaNULL;
jQuery("#grdSigTop").jqGrid('setGridParam', { url: URL, page: 1 }).trigger("reloadGrid");
}
L'únic a destacar d'aquest exemple és com recuperar el valor de checkboxes amb jQuery.
#07/02/2014 13:20 Programació Javascript Autor: Alex Canalda
La majoria de formularis tenen "desplegables" en el desenvolupament web, o "combos" o "select", depenent del programador es dóna un nom o un altra, jo els dic de qualsevol de les opcions, segons el dia. Però tots volen dir el mateix. Ja he explicat que en la part client que els "select" no tenen els valors carregats en el HTML (a menys que siguin un Sí/No, o Blanc/Negre...), que és posteriorment que es carreguen les dades. És durant la creació dels controls que es posen els valors. Quan hi ha "selects" dels que s'anomenen en cascada (el valor d'un primer "select" determina les opcions d'un segon) també hi ha crides a aquesta funció. En la part servidora hi ha un ASHX que es dedica només a carregar els "selects", el comboloader.ashx. És lògic doncs que a la part client, al Javascript hi hagi una funció homònima que s'encarregui d'enviar peticions i rebre les respostes posant als "select" les seves opcions corresponents. La funció es diu LoadCombos, i té un munt de paràmetres, va començar amb pocs i a mesura que ha passat per projectes s'han anat afegint.
pFrm: Identificador del formulari on estan els combos.
pCombos: Identificadors dels "select" que s'han de carregar. Aquests noms han de coincidir amb els que tingui el comboloader.ashx. Si hi ha més d'un van separats per guions.
pURLExtra: Alguns combos han de tenir els seus valors restringits per algun valor, es posa aquí en el format "&nom=valor" (pot ser més llarga "&nom1=valor1&nom2=valor2..., important el detall del "&" inicial).
pAsync: indica si es poden carregar els combos de forma asíncrona. Això passa en la funció CrearteNNNNEvents, que a vegades s'han de carregar els combos per diferents formularis, aleshores si no es vol que les crides es solapin (lleig) s'ha de posar a false.
pValorDefecte: Es un JSON que indica els valors per defecte de diferents combos, té un aspecte com {"filtre_Comunitat":"3"} i es poden posar els diferents valors per defecte dels diferents combos.
pPostLoadFunction: funció que s'executa després de carregar els combos. Posar "undefined" si no es vol.
pOptionBuit: booleà que indica si als combos s'ha de posar una primera opció que estigui buida.
pValorOptionBuit: valor que té l'opció buida, normalment ''
Com sempre el codi:
var lastFrm = "";
var lastCombos = "";
var lastValorsDefecte = null;
function LoadCombos(pFrm, pCombos, pURLExtra, pAsync, pValorDefecte, pPostLoadFunction, pOptionBuit, pValorOptionBuit) {
if (pURLExtra === undefined) pURLExtra = "";
if (pAsync === undefined) pAsync = true;
if (pOptionBuit === undefined) pOptionBuit = false;
lastFrm = pFrm;
lastCombos = pCombos;
if (pValorDefecte !== undefined) lastValorsDefecte = eval('(' + pValorDefecte + ')');
else lastValorsDefecte = null;
$.ajax({
type: "GET",
async: pAsync,
dataType: 'json',
url: "comboloader.ashx?combos=" + pCombos + pURLExtra,
success: function(data, textStatus) {
if (!data.ERROR) {
var ComboID = '';
var options = '';
var ComboOBJ = null;
jQuery.each(data, function(i, val) {
ComboID = val.ComboID;
options = '';
if (val.Items) {
if (pOptionBuit) options = '<option value="' + pValorOptionBuit + '"></option>';
jQuery.each(val.Items, function(j, ItemVal) {
options += '<option value="' + ItemVal.Val + '">' + ItemVal.Disp + '</option>';
});
//Es més eficient fer-ho així però en IE7 no funciona bé.//$("#" + lastFrm + " #" + ComboID).html(options);
$("#" + lastFrm).find("#" + ComboID).html(options);
if (lastValorsDefecte != null) {
for (var member in lastValorsDefecte) {
if (member == ComboID) $("#" + lastFrm).find("#" + ComboID).val(lastValorsDefecte[member]);
}
}
}
else {
$("#" + lastFrm).find("#" + ComboID).html('');
}
});
if (pPostLoadFunction !== undefined) pPostLoadFunction(data);
}
else {
$("#ui-dialog-title-dialogErr").html("Error carregant combos");
$("#errmsg").html(data.ERROR);
$("#dialogErr").dialog("open");
}
}
});
}
Unes invocacions d'exemple podrien ser, aquesta primera és complicada, carrega un munt de combos al formulari filtre, i el combo de les comarques es filtra per la comunitat 3, i clar el combo comunitats ha de tenir aquest valor per defecte:
Un exemple de combos encadenats pot ser com la següent, s'assigna a l'event "canvi" del combo origen i estaria a la funció "createNNNNNEvents" corresponent:
Al llarg dels últims posts de desenvolupament web, de la part client, m'he anat referint a l'estat del formulari. És hora d'aclarir que és. El formulari, tal com està plantejat pot estar en dos estats. El primer és l'estat "actualitzar" ("update"), on el formulari té carregades dades, s'editen i un cop s'acaba amb el botó "Guardar" s'envien a la part servidora (al ASHX corresponent) per guardar-les. Al guardar les dades seguim en aquest estat.
L'altra estat és el "insertar" on el formulari està buit (sense dades), s'omple i s'envien al ASHX per guardar-les. En aquesta operació es recupera del ASHX la clau primària generada per en futures edicions fer actualitzacions ja que el formulari passa a l'estat "actualitzar".
Quan des de l'estat "actualitzar" es fa una eliminació, el formulari es neteja i passa a l'estat "insertar".
El botó "Nou" (per crear un nou registre) també posa al formulari en l'estat "insertar".
Cada un d'aquests estats té una funció Javascript que posa els controls del formulari com els correspon. Veiem uns exemples, mode insert:
function ClassificacioInsert() {
//Neteja del formulari
$('#frmDetall').validate().resetForm();
$('#frmDetall :input')
.not(':button, :submit, :reset, :checkbox')
.val('')
.removeAttr('selected');
$("#frmDetall :checkbox").removeAttr('checked');
//Reasignació del boto Guardar, amb action INSERT
$("#frmDetall #btnSave").unbind('click');
$("#frmDetall #btnSave").click(function ()
{ SubmitFrm('#frmDetall', 'classificacions.ashx?action=insert', true, ClassificacioPostInsert);
});
//El boto Eliminar desapareix en un alta nova
$("#frmDetall #btnDelete").hide();
//Exemple de valor per defecte d'un controlvar Avui = new Date();
$("#frmDetall #DataAlta").val(Avui.getDate() + "/" + (Avui.getMonth() + 1) + "/" + Avui.getFullYear());
//Altres botons es deshabiliten
$("#frmDetall #btnDeleteEspecifica").attr('disabled', '');
$("#frmDetall #btnAfegirEspecifica").attr('disabled', '');
$("#frmDetall #autoEspecifica").attr('disabled', '');
$("#frmDetall #autoEspecifica_TC").attr('disabled', '');
$("#frmDetall #btnDeleteTAAD").attr('disabled', '');
$("#frmDetall #btnAfegirTAAD").attr('disabled', '');
$("#frmDetall #afegirTAAD").attr('disabled', '');
$('#PK_TAAD').val('');
$('#afegirTAAD').html('');
//Es neteja la clau primària, quan es guardi el registre//ja es posara el valor correcte
$('#PK_Classificacio').val('');
evPK_Classificacio = "";
}
Posar en mode "actualitzar" pot tenir una funció com aquesta:
function ClassificacioUpdate(pID) {
//Es netegen validacions pendents
$('#frmDetall').validate().resetForm();
//Si arriba la PK informada es posa a la variableif (pID !== undefined) $("#frmDetall #PK_Classificacio").val(pID);
//S'activen alguns controls (això depen de la lògica de negoci)
$("#frmDetall #btnDeleteEspecifica").removeAttr('disabled');
$("#frmDetall #btnAfegirEspecifica").removeAttr('disabled');
$("#frmDetall #autoEspecifica").removeAttr('disabled');
$("#frmDetall #autoEspecifica_TC").removeAttr('disabled');
//Es mostra el botó d'Eliminar
$("#frmDetall #btnDelete").show();
//Es posa el guardar en "update"
$("#frmDetall #btnSave").unbind('click');
$("#frmDetall #btnSave").click(function() {
SubmitFrm('#frmDetall', 'classificacions.ashx?action=update', true, ClassificacioPostUpdate);
});
}
#06/02/2014 11:40 Programació Javascript Autor: Alex Canalda
Dins les aplicacions web, ha d'haver-hi alguna forma d'enviar les peticions d'eliminació de registre de la part client a la part servidora (als ASHX). En Javascript faig servir una crida Ajax de jQuery per fer aquesta tasca. De fet crec que és la part més senzilla de tot plegat.
En el post de la creació de controls ja hi ha el codi que assigna al botó "Eliminar" la funció que s'encarrega de fer la eliminació (valgui la redundància). Aquí es tracta d'explicar en detall la funció DeleteElem.
DeleteElem rep varis paràmetres:
pForm: El identificador del formulari on està el registre que es vol eliminar
pURL: la URL que ha d'invocar en la part servidora, normalment conté el paràmetre action amb el valor "delete"
pID: la clau primària que serveix per invocar l'eliminació. Si està composta per diferents camps cal posar-los separats per un guió "-" i s'han de correspondre al camps del formulari amb el mateix nom.
pPostDelete: un booleà que indica si ha de cridar a una funció posterior a l'eliminació.
pFunctionPostDelete: la funció posterior a la eliminació.
pMissatge: un missatge que surt per pantalla un cop s'ha eliminat (per defecte surt un lacònic "Element eliminat correctament").
Com sempre el codi:
function DeleteElem(pForm, pURL, pID, pPostDelete, pFunctionPostDelete, pMissatge) {
var Missatge = "Element eliminat correctament";
if (pPostDelete === undefined) pPostDelete = false;
if (pMissatge !== undefined) Missatge = pMissatge;
var PK = pID.split("-");
var PKURL = "";
jQuery.each(PK, function(i, val) {
PKURL += "&" + val + "=" + $("#" + val).val();
});
$.ajax({
type: "GET",
url: pURL + PKURL,
success: function (data, textStatus) {
if (data.indexOf("ERROR:") != -1) {
$("#ui-dialog-title-dialogErr").html("Error eliminant dades");
$("#errmsg").html(data);
$("#dialogErr").dialog("open");
}
else {
$("#ui-dialog-title-dialogOK").html("Element eliminat");
$("#okmsg").html(Missatge);
$("#dialogOK").dialog("open");
if (pPostDelete) {
if (pFunctionPostDelete !== undefined) pFunctionPostDelete(data);
}
}
}
});
}
Normalment la funció postDelete el que fa és netejar el formulari i posar-lo en mode inserció. Seria algo així:
function BlogPostDelete(pData) {
jQuery("#grdPosts").jqGrid().trigger("reloadGrid");
PostsInsert();
}
#05/02/2014 17:45 Programació Javascript Autor: Alex Canalda
Una part fonamental del desenvolupament web és guardar les dades. En la part client això consisteix en enviar les dades a la part servidora, en el cas que ens ocupa els ASHX.
Com tots els formularis de l'aplicació web envien les dades de la mateixa manera, tots fan servir la mateixa funció: SubmitFrm.
Aquesta funció és força senzilla i fa servir la serialització de jQuery per empaquetar els camps d'un formulari i posteriorment enviar-los al servidor amb Ajax. Normalment la URL on s'envia porta informat el paràmetre "action" amb el valor "update". D'aquesta manera la part servidora fa un UpdateData que invoca a un UPD de la clsDades (bé de fet de la clase derivada corresponent).
A vegades també és necessari efectuar alguna operació un cop s'han guardat les dades. Per exemple recarregar el grid perque refresqui els valors o bé posar el valor de la clau primària generada en un "insert" a la variable que toca. Per això estan els paràmetres pPostSubmit i pFunctionPostSubmit. El primer és un booleà que indica si s'ha de fer algo més després de guardar les dades i el segon és la funció que s'ha de cridar.
Normalment aquesta funció SubmitFrm es crida des del botó Guardar.
function SubmitFrm(pForm, pURL, pPostSubmit, pFunctionPostSubmit) {
if (pPostSubmit === undefined) pPostSubmit = false;
//El formulari ha de validar per enviarif ($(pForm).validate().form()) {
//Serialitzacióvar frmdata = $(pForm).serialize();
//Ajax
$.ajax({
type: "POST",
url: pURL,
data: frmdata,
dataType: "text",
contentType: "application/x-www-form-urlencoded; charset=ISO-8859-1",
success: function (data, textStatus) {
if (data.indexOf("ERROR:") != -1) {
$("#ui-dialog-title-dialogErr").html("Error guardant les dades");
$("#errmsg").html(data);
$("#dialogErr").dialog("open");
}
else {
$("#ui-dialog-title-dialogOK").html("Dades guardades correctament");
$("#okmsg").html("Dades guardades correctament.");
$("#dialogOK").dialog("open");
//Crida a la funció PostSubmitif (pPostSubmit) {
if (pFunctionPostSubmit !== undefined) pFunctionPostSubmit(data);
}
}
}
});
}
}
Una funció PostSubmit d'exemple podria ser:
function ClassificacioPostInsert(pData) {
evPK_Classificacio = pData;
jQuery("#grdClassificacions").jqGrid().trigger("reloadGrid");
ClassificacioUpdate(pData);
}
Aquesta funció actualitza la variable global que té la clau primaria (PK), refresca el grid i posa el formulari en mode "actualitzar". Dins del paràmetre pData arriba la PK que és l'únic que envia un Insertar desde la part servidora. Una funció NNNNNNNPostUpdate és més senzilla ja que només té la línia que refresca el grid.
#04/02/2014 11:54 Programació Javascript Autor: Alex Canalda
Dins del desenvolupament web, en el javascript de la part client, cal carregar dades en un formulari per editar-les. És aleshores quan entra en joc aquesta funció.
Aquesta funció és relativament senzilla, el primer que fa és netejar els controls del formulari, després fa una crida Ajax a la part servidora, normalment a un ASHX amb el paràmetre "action" amb el valor "data". Això fa que la part servidora faci una crida a la funció LoadData (de la part servidora) i obtingui les dades. Aquestes es serialitzen al ASHX i s'envien al client, on es deserialitzen col·locant els valors corresponents als controls del formulari.
Cal recordar que perque aquesta deserialització funcioni i cada control rebi el valor que li pertoca cal que el control tingui el mateix nom que el camp del JSON. Val més una imatge que mil paraules:
Un cop deserialitzades les dades el formulari s'ha de posar en mode "actualitzar", de tal manera que a l'apretar el botó "Guardar" faci una actualització contra el ASHX corresponent. Com sempre el codi:
function LoadClassificacioData(pPK_Classificacio) {
//Actualització variable global que guarda la clau primària (PK)
evPK_Classificacio = pPK_Classificacio;
//Neteja del formulari
$('#frmDetall :input')
.not(':button, :submit, :reset, :checkbox')
.val('')
.removeAttr('selected');
$("#frmDetall :checkbox").removeAttr('checked');
//Recuperació
$.ajax({
type: "GET",
url: "classificacions.ashx?action=data&PK_Classificacio=" + pPK_Classificacio,
success: function(data, textStatus) {
if (!data.ERROR) {
var config = {};
config.isPHPnaming = false;
config.overwrite = true;
//Deserialització
$('#frmDetall').deserialize(data, config);
//Mode update
ClassificacioUpdate();
}
else {
alert("ERROR: " + data.ERROR);
}
}
});
}
#03/02/2014 11:12 Programació Javascript Autor: Alex Canalda
En la part client, un cop carregat l'HTML el que cal fer és assignar-li events i crear els controls que han d'anar al formulari. Això només cal fer-ho un cop. Per això poso una funció CreateNNNNNEvents (on NNNNN és el nom de l'entitat que estem tractant, o el del formulari). Aquesta funció no té paràmetres, i es crida des del document.ready. Un exemple de document.ready, on també es crea el menú:
<script type="text/javascript">
$(document).ready(function() {
ddsmoothmenu.init({
mainmenuid: "smoothmenu1", //menu DIV id
orientation: 'h', //Horizontal or vertical menu: Set to "h" or "v"
classname: 'ddsmoothmenu', //class added to menu's outer DIV
contentsource: "markup"//"markup" or ["container_id", "path_to_menu_file"]
});
CreateClassificacioEvents();
});
</script>
Ara cal veure un exemple de la creació de controls, primer la càrrega de combos:
function CreateClassificacioEvents() {
LoadCombos("frmFiltre", "PK_Arxiu", "", false, undefined, undefined, true, '');
LoadCombos("frmDetall", "afegirTAAD", "", true, undefined, undefined, true, '');
//... TO BE CONTINUED...
La funció LoadCombos està definida en el helper.js, i només ella soleta mereix un post apart.
El que sí es pot comentar en aquesta crida és que només es carrega un combo per cada formulari, si un formulari en té més d'un aleshores es posen separats per guions. El màxim que he vist han estat 9 combos en un formulari. En el post de la funció LoadCombos s'especifiquen cadascun dels seus paràmetres. Un cop carregats els combos cal posar la resta de controls, per exemple els calendaris:
Aquests calendaris estan localitzats (tenen un idioma en concret, el "ca"-català), però no tenen la part de l'hora. Si es volen amb hora cal veure el post dels calendaris amb hora.
Aquesta és la instanciació d'un grid. Es defineixen les columnes, el ASHX d'on es treuen les dades amb l'acció "loadgrid" de la part servidora. L'event important és quan es fa clic en una fila, "onSelectRow", la funció LoadNNNNNNData recuperarà les dades amb Ajax per omplir el formulari detall (en el post de la part client es veu quin és aquest formulari).
Definim un autocomplete, els autocomplete normalment tenen una action autocompleteNNNN en el ASHX corresponent. Un cop a la part servidora habitualment es fa servir un GET que tindrà un paràmetre que farà un LIKE a la BBDD, per evitar que es saturi el propi control té una cache, i per evitar que retorni massa resultats ajustem la propietat minChars.
Per costum (no per res) poso els events click dels botons al final. El botó d'eliminar registre fa el mateix a tots els formularis, la funció DeleteElem está al helper.js.
Una cosa que s'espera d'un formulari de cerca és que al apretar "enter, intro, return" el formulari es posi en marxa. El codi assigna a tots els inputs del formulari de filtrar l'event corresponent per fer això. Finalment es posa el formulari en mode inserció perque no té cap registre carregat, quan es faci clic en algun element del grid aleshores es passa a mode edició.
Aquests events i controls són els més típics, però cada formulari és un cas i el més habitual és hi hagi moltes més coses definides. Per exemple un multiple upload de fitxers (imatges).
#29/01/2014 16:36 Programació Javascript Autor: Alex Canalda
Dins de les aplicacions web, el Javascript de la part client és el que més costa de fer. I és cert, és la que acostuma a donar problemes. Els problemes que m'acostumo a trobar són incompatibilitats entre navegadors. Això amb el jQuery es mitiga força, però igualment algun problema es troba. De moment no he trobat cap que sigui irresoluble, simplement són una molèstia amb la que s'ha de viure.
Dins del Javascript hi ha dues parts diferenciades, una que crea els events i controls en l'esquelet HTML i una altra part que gestiona el funcionament del formulari un cop està en marxa.
Hi ha controls força senzills, que amb una línia es creen, altres necessiten una bona parrafada (per exemple un grid). Tota la creació de controls normalment es realitza en la funció que anomeno "CreateNNNNEvents", on NNNNN és el nom de la pantalla on està el Javascript. Aquesta funció només s'executa una única vegada. Un cop creats els controls el que acostuma a passar és que es modifiquin, per exemple si es filtra un grid, si el valor d'un combo restringeix els valors d'un altra combo, etc...
Hi ha tot un conjunt de funcions importants que com són sempre les mateixes, estan agrupades en el helper.js. Aquestes funcions són les encarregades de serialitzar/deserialitzar les dades per enviar-les a la part servidora, carregar els combos (es a dir invocar al comboloader i deserialitzar els resultats posant-los als combos corresponents).
#29/01/2014 13:05 Programació Javascript Autor: Alex Canalda
Dins del desenvolupament d'aplicacions web, ja hem tractat la part de BBDD, la part servidora amb els ASHX i la clsDades. Ara cal començar amb la part client.
Sempre m'ha molestat la barreja que es fa en la part client entre les diferents parts d'una pàgina web. Tal com ho he muntat he aconseguit separar la presentació (HTML+CSS), els events (jQuery i Javascript) i les dades (Ajax pel transport i JSON).
Amb aquesta separació m'he simplificat la vida i millorat la eficiència (per la xarxa no viatja HTML amb dades empotrades, només JSON), el HTML només viatja el primer cop que es carrega. Si cal canviar colors, tocar HTML o CSS, que cal modificar el comportament, tocar Javascript, que cal tocar dades, aleshores ja depen, però normalment JSON, ASHX i llestos.
Pel que fa a components de .NET, per mi estan morts. Són una castanya, suposo que amb el temps han millorat però recordo que eren difícils de fer servir... Així que faig servir uns altres:
El autocomplete, encara que està deprecat al principi el de jQuery no limitava els resultats i per això el vaig mantenir un temps. Ara ja sí es poden limitar els resultats del autocomplete als de la llista (similar un combobox) i faig servir el de jQueryUI.
Un menú horitzontal (encara que admet un setup vertical)
Un deserialitzador fet en jQuery
El validador de formularis del jQuery
Al fer servir aquests components també té una altra avantatja. És que si algun dia la part servidora deixa de ser .NET i és PHP o qualsevol altra tecnologia, mentre es respecti l'estructura del JSON, almenys la part client seguirà funcionant.
Un petit formulari quedaria com la següent imatge:
Aquest és un formulari tipus, poden variar moltes coses, per exemple que el detall sigui molt gran i necessiti una pantalla apart, que el filtre enlloc d'un camp tingui 19, que el grid el vulguin molt gran, que hagin combos depenents, grids depenents un de l'altra, etc... Tot es pot fer. També té una avantatja que és que no necessita mantenir el ViewState de .NET, estalviant molt tràfic de dades, només viatja el JSON de les poques dades del formulari, el JSON del grid, etc... Així que ara el que cal es veure les parts que he comentat en els seus corresponents posts.
Doncs no existeix, només hi el datetime picker, que com el seu nom indica només permet escollir la data, sense hora (notis la ironia).
Per sort en Trent Richardson ha creat un "addon" molt aconseguit que aconsegueix gestionar l'hora en el datetime picker. Cal afegir als CSSs un fitxer, i un altra al javascript del projecte i llestos. Per invocar-lo es fa així
Com es pot veure amb uns sliders permet escollir l'hora [0-23] i els minuts [0-59], com sempre el calendari permet editar el seu valor directament (i s'actualitza el control) i a la inversa.
#12/11/2013 10:00 Programació Javascript Autor: Alex Canalda