Eliminar prototype¶
DOCUMENTO EN CONSTRUCCIÓN
- Índice de contenidos
- Eliminar prototype
- Objetivo y descripcion.
- Metodologia de trabajo.
- Eliminar prototype-helper. Interacción AJAX.
- Helpers generales
- Resumen de nuevos fiecheros, y cambios importantes
- Chuleta de cambios.
- sin_prototype_1
Objetivo y descripcion.¶
Hasta la fecha (2018) gong / gor ha basado buena parte de su comportamiento-front-end en la libreria de javascript prototype ( https://github.com/sstephenson/prototype/tree/master )
El objetivo que nos planteamos en este ciclo de desarrollo es eliminar esta librería y sustituirla, por javascript con JQuery.
Recojemos en este documento algunas claves de la propuesta de cambio de librerías para que sirva para su discusión y como documentación del proceso
Metodologia de trabajo.¶
Rama sin_prototype¶
Para el desarrollo de las nuevas funcionalidades sin prototype se utilizara como rama de trabajo sin_prototype:
https://git.semillasl.com/gong/gor/tree/sin_prototype
Sobre esta rama ser irán haciendo "merges" del master conforme gor pudiese avanzar en paralelo a las modificaciones que haremos en esta rama para susituir prototype. Es decir cada vez que se produzcan cambios en la rama master se hara un merge de estos cambios a al rama "sin_prototype" y se actuzalizará el repositorio
git checkout sin_prototype git merge master git push origin sin_prototype
Conceptos generales.¶
En este proceso de eliminación de prototype (y todos los componente basado en él) nos hemos guiado por las siguientes ideas generales.
- No se modifica ni el comportamiento ni la presentación general (solo pueden darse pequeños cambios en función de las necesidades/limitaciones de los nuevos componentes)
- Se intenta cambiar el menor código posible en todo el sistema. Se sustituyen los métodos que existen por unos nuevos sin tener que modificar el uso a lo largo del código de dichos métodos. Se incumple esta idea para evitar el hecho de tener código javascript dentro del contenido (ver punto posterior)
- La elimnación de prototype no se mezcla con otros procesos de refactorización y/o re-ordenación del código. Por ejemplo: si existen dos métodos en el helper para un mismo tipo de elemento/comportamiento (que están conviviendo, y que "hace tiempo" que podrían unificarse) se modificarán los dos y no se intentará unificar. Las situaciones que se encuentren de este tipo se vuelcan en tickets específicos de cara a una futura refactorización.
- No se hace ningún cambio de diseño.
Respecto al nuevo javascript
- No se mezcla javascript con los contenidos ni con la presentación. JavaScript not obstrusive: "Separación de la funcionalidad JavaScript (la "capa del comportamiento") de las capas de estructura/contenido y de presentación de una página".
- Se utiliza, los componente de jquery-ui (widgets: dialog, tooltip, ), sin introducir nuevos componentes mas avanzados en diseño. Ejemplo: no introducimos bootstrap.
- Las referencias a jQuery se mantienen todas dentro de herramientas_gong.js y gong_ajax.js. En los helpers, y por lo tanto el javascript que tenemos en la DOM se utiliza javascript sin ninguna librería adicional ("vanilla javascript" o "javascript puro"). NOTA: En definitiva, si alguna vez se quisiese cambiar la librería que nos aporta los comportamientos "adicionales" de javascript (en este caso jQuery) solo habria que cambiarlo en herramientas_gong.js o en ajax_gong.js
Finalmente no se va a llevar a cabo ningun proceso de re-factorización, ni de unificación de comportamiento.
NOTA: concretamente existen diferentes modelos de usabilidad (al menos dos grandes: modales o formularios inline) que conviven y que no van a ser unificados en este proceso.
Tickets.¶
El trabajo se guia a traves de los tickets del gitLab:
https://git.semillasl.com/gong/gor/issues
En el futuro, y para sacar la versión definitiva, se utilizará como habitualmente el redmine para crear algun "gran" ticket que deje constancia del cambio de versión.
Tag sin_prototype_05¶
Se utiliza el tag sin_prototype_05 para:
- Mostrar como quedará el codigo y los cambios en todos los helpers y lo javascript
- Mostrar los cambios que se requerirán en el código a traves de 5 casos de uso que se toman como modelo de como deberia cambiar el resto del codigo (ver: https://git.semillasl.com/gong/gor/issues/15 )
Eliminar prototype-helper. Interacción AJAX.¶
Antiguo funcionamiento¶
Hasta le fecha en GONG se había utilizado prototype-helper para la comunicación vía AJAX. Sobre los métodos que ofrecía prototype-helper, se habían construido unos métodos propios recogidos en:
https://git.semillasl.com/gong/gor/tree/gong-gor-4.01/app/helpers/nueva_edicion_helper.rb#L250-252
Se trata de metodos estilo nueva_fila:
https://git.semillasl.com/gong/gor/tree/gong-gor-4.01/app/helpers/nueva_edicion_helper.rb#L286
def nueva_fila valores={}
page.remove "formularioinline", "formulariofondo", "formulariocontenedor"
valores[:locals][:update] = valores[:nueva_fila] if valores[:locals]
page.insert_html :after, valores[:update], :partial => valores[:partial] , :locals => valores[:locals]
page.visual_effect :highlight, valores[:nueva_fila], :duration => 5
page.mensaje_actualizacion valores[:nueva_fila], valores[:mensaje]
end
Estos metodos se utilizaban desde el controlador cuando se hace una peticion AJAX, entonces, el controlador haciendo uso de los metodos del prototype-helper, o haciendo uso de los metodos especificados en "nueva_edicion_helper.rb", como el que acabamos de describi envia una secuencia de instrucciones Javascript (usando prototype) que se "inyectaban" en la pagina del cliente. Un ejemplo sería:
https://git.semillasl.com/gong/gor/blob/master/app/controllers/matriz_controller.rb#L127
def editar_nuevo_objetivo_general
@objetivo_general = @proyecto.objetivo_general || ObjetivoGeneral.new
render(:update) { |page| page.formulario :partial => "formulario_objetivo_general", :update => params[:update] }
end
Nuevo funcionamiento.¶
En el nuevo funcionamiento se va a evitar enviar codigo javascript, que posteriormente tiene que ser ejecutado en el navegador. Como habiamos comentado en el modelo prototype los controladores enviaban ("render") codigo javascript que era el encargado de modificar la pagina (cerrar formularios, actualizar filas,... etc)
Para la interaccion ajax con el usaurio ahora tenemos libreria javascript que se carga en el navegador y que se encarga de la comunicación con el servidor:
https://git.semillasl.com/gong/gor/tree/sin_prototype_05/app/assets/javascripts/ajax_gong.js
Como se puede ver esta libreria recogera una serie de acciones y sera la encargada de gestionarlas:
https://git.semillasl.com/gong/gor/tree/sin_prototype_05/app/assets/javascripts/ajax_gong.js#L137
function renderResult(parsedData) {
jQuery.each(parsedData, function(index, data) {
data["action"] == "remove" && removeElement(data);
data["action"] == "replace" && replaceElement(data);
data["action"] == "replace_html" && replaceHtml(data);
data["action"] == "actualiza_modal" && actualizaModal(data);
data["action"] == "modifica_value" && modificaValue(data);
data["action"] == "insert_html_after" && insertHtmlAfter(data);
data["action"] == "insert_html_before" && insertHtmlBefore(data);
data["action"] == "show" && showElement(data);
data["action"] == "hide" && hideElement(data);
data["action"] == "call" && HERRAMIENTAS_GONG.ejecutarFuncion(data["func"]);
data["action"] == "highlight" && highlightElement(data);
data["action"] == "fade" && removeElement(data);
data["action"] == "add_class" && addClass(data);
data["action"] == "remove_class" && removeClass(data);
data["action"] == "visual_effect" && visualEffect(data);
});
};
Estas acciones le llegaran al navegador del usuario en forma de una estructura de datos json. Esta estructura tendra por ejemplo esta forma:
[ {action: "remove", update: "formulariocontenedor", delay: 0}, {action: "insert_html_after", update: "presupuesto_sub_1_1", <contenido_del_partial> } {action: "highlight", update: "presupuesto_sub_1_1", duration: 4} ]
Así hemos sustituido los helpers que enviaban código por helpers que crean una estructura de datos de tipo json con las acciones que interpretará en ajax_gong.js que se ha cargado en el navegador previamente. Es decir, el controlador y el ajax_gong, se comunicacion a través de un json con las acciones a realizar.
Estas acciones se encutran re-escritras en app/serializer/json_gong.rb:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/serializers/json_gong.rb
En este fichero tenemos re-escritos los metodos del nueva_edicion_helper.rb relativos a la comunicacion ajax. Utilizando el mismo ejemplo de antes asi recogemos el metodo nueva_fila:
https://git.semillasl.com/gong/gor/tree/sin_prototype_05/app/serializers/json_gong.rb#L10
def nueva_fila valores={}
remove "formularioinline"
remove "formulariofondo"
remove "formulariocontenedor"
valores[:locals][:update] = valores[:nueva_fila] if valores[:locals]
insert_html :after, valores[:update], :partial => valores[:partial] , :locals => valores[:locals]
visual_effect :highlight, valores[:nueva_fila], :duration => 5
mensaje_actualizacion valores[:nueva_fila], valores[:mensaje]
end
Y por otro lado tenemos re-escritos los metodos propios de prototype-helper de esta forma:
def hide(*options)
@obj.push({action: 'hide', update: options[0]})
end
Como se puede ver finalmente lo que estamos haciendo es meter en el @obj json que vamos a enviar al navegador, las acciones que se van a ejecutar.
Cambios en los controladores para el comportamiento AJAX¶
Cambio general ( render :update > JsonGong.new(self) )¶
Lo que hay sustituir en los controlador es el "render update page" utilzado hasta la fecha
Por ejemplo el metodo que hemos presentado antes de matriz controller:
https://git.semillasl.com/gong/gor/tree/gong-gor-4.01/app/controllers/matriz_controller.rb#L127
def editar_nuevo_objetivo_general
@objetivo_general = @proyecto.objetivo_general || ObjetivoGeneral.new
render(:update) { |page| page.formulario :partial => "formulario_objetivo_general", :update => params[:update] }
end
Quedaria transformado en:
https://git.semillasl.com/gong/gor/tree/sin_prototype_05/app/controllers/matriz_controller.rb#L127
def editar_nuevo_objetivo_general
@objetivo_general = @proyecto.objetivo_general || ObjetivoGeneral.new
JsonGong.new(self) { |page| page.formulario :partial => "formulario_objetivo_general", :update => params[:update] }
end
Pasar lo RJS al controlador.¶
En ocasiones el comportamiento del javascript de un render estaba dentro de los ficheros rjs
https://git.semillasl.com/gong/gor/tree/gong-gor-4.01/app/views/comunes/anadir_actividad.rjs
Ahora lo pasaremos al controlador:
JsonGong.new(self) do |page|
page.replace "resultado_" + params[:linea], :partial => "comunes/resultado", :locals => { :linea => params[:linea].to_i, :ultima => true}
end
Sustituir las referencias a MB_content o Modalbox.algun_metodo¶
Las referencias a "MB_content" no son necesarias. Directamente se le puede pasar al metodo "actuliza_modal" el contenido.
NOTA: Por otro lado el codigo javascript que se llama a traves de page.call("Modalbox.resizeToContent") se puede mantener o eliminar dado que al actualizar_modal ya se hace una resize de la misma
Asi por ejemplo una actualizacion de contenidos como esta:
render :update do |page|
page.replace_html params[:atributo], :partial => vista, :locals => { :relacion => params[:atributo] , :relaciones => @relaciones}
page.visual_effect :highlight, params[:atributo] , :duration => 6
page.replace_html 'MB_content', :inline => '<%= mensaje_error(@objeto, :eliminar => true) %><br>'
page.call("Modalbox.resizeToContent")
end
se quedará de la forma:
JsonGong.new(self) do |page|
page.replace_html params[:atributo], :partial => vista, :locals => { :relacion => params[:atributo] , :relaciones => @relaciones}
page.visual_effect :highlight, params[:atributo] , :duration => 6
page.actualiza_modal :inline => '<%= mensaje_error(@objeto, :eliminar => true) %><br>'
end
Pendiente: definir hide_modal, resize_modal.
page[:identificador].value > modifica_value¶
Las llamadas a traves de un identificador para sustitur el value de tipo:
page[:presupuesto_importe].value = numero_unidades * coste_unitario if numero_unidades && coste_unitario
Se sustituyen por:
page.modifica_value("presupuesto_importe",(numero_unidades * coste_unitario)) if numero_unidades && coste_unitario
Nombre completo de los templates.¶
Este caso debería ser mejor verificado
En ocasiones (cuales?) es necesario cambiar la ruta completa del template, en casos como este:
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/controllers/documento_controller.rb#361
page.formulario :partial => "formulario_asociar_o_nuevo", :tipo => params[:tipo], :objeto_id => params[:objeto_id], :update => params[:update]
Se completa la informacion añadiendo la ruta completa del template:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/documento_controller.rb#361
page.formulario :partial => "documento/formulario_asociar_o_nuevo", :tipo => params[:tipo], :objeto_id => params[:objeto_id], :update => params[:update]
Helpers generales¶
Cambiar los "loading" y "complete"¶
Todos los trozos de codigo Javascript relativos al loading y al complete que se pasaban a los helpers ajax (link_to_remote, observe_field). Se puede optar por dos
- Eliminar la referencia. Por defecto el sistema mostrara el spinner del identificar "#espera" del layout general.
- Pasar la referencia con un formato de tipo:
loading: { show: [ identificador1, identificador2,.. ] }, complete: {hide: [ identificador1, identificador2,.. ] }
Por ejemplo:
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/helpers/nueva_edicion_helper.rb#L114
def icono_vinculo url, imagen, titulo, otros={}
link_to_remote icono( imagen, titulo ), :url=> url, :loading => "Element.show('espera');", :complete => "Element.hide('espera');"
end
Quedarán de esta forma:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/nueva_edicion_helper.rb#L149
def icono_vinculo url, imagen, titulo, otros={}
link_to_remote( icono( imagen, titulo ), :url=> url, :loading => {show: ['#espera']}, :complete => {hide: ['#espera']} )
end
O mejor de esta forma (que en este caso son equivalentes):
def icono_vinculo url, imagen, titulo, otros={}
link_to_remote( icono( imagen, titulo ), :url=> url )
end
link_to_remote¶
Se ha sustituido el helper incluido en prototype-helper link_to_remote por un metodo en el application_helper:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb#L61
def link_to_remote( etiqueta, opciones = {} )
opciones[:url] = url_for(opciones[:url])
# falta el param submit (app/views/presupuesto_detallado/_presupuesto_detallado.html.erb:55)
if opciones[:html]
opciones[:id] = opciones[:id] || opciones[:html][:id]
opciones[:class] = opciones[:class] || opciones[:html][:class]
opciones[:style] = opciones[:style] || opciones[:html][:style]
end
if opciones[:id]
opciones[:id] = asegurar_id(opciones[:id])
else
opciones[:id] = SecureRandom.hex(7)
end
cadena = link_to( etiqueta, '#', id: opciones[:id], class: opciones[:clase], style: opciones[:style] )
cadena << content_tag( :script, type: "text/javascript" ) do
"document.getElementById(\"#{opciones[:id]}\").addEventListener('click', function(event) {
event.preventDefault();
HERRAMIENTAS_GONG.linkToRemote(#{opciones.to_json});
});".html_safe
end
return cadena
end
Como se puede observar se introduce una referencia al modulo javascript HERRAMIENTAS_GONG. En HERRAMIENTAS_GONG se encuentra recogidas diferentes elementos nuevos de Javascript. En este caso por ejemplo sería:
self.linkToRemote = function(opciones) {
opciones.hasOwnProperty("url") && AJAX_GONG.updateAjax(opciones);
};
observe_field¶
Ha sido re-escrito en el application_helper de la siguiente forma:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb#L42
def observe_field( field_id, opciones = {} )
if opciones[:function]
cadena = content_tag( :script, type: "text/javascript" ) do
"document.getElementById(\"#{field_id}\").addEventListener('change',function(){
HERRAMIENTAS_GONG.ejecutarFuncion(#{opciones[:function].to_json});
});".html_safe
end
else
opciones[:url] = url_for(opciones[:url]) if opciones[:url]
opciones[:field_id] = field_id
cadena = content_tag( :script, type: "text/javascript" ) do
"document.getElementById(\"#{field_id}\").addEventListener('change',function(){
HERRAMIENTAS_GONG.observarCampo(#{opciones.to_json});
});".html_safe
end
end
return cadena
end
El nuevo obser_field utiliza el metodo ejecutarFuncion si se le pasa el parametro :function:
self.ejecutarFuncion = function(funcion) {
var func = new Function(funcion);
func();
};
o observarCampo cuando nos se pasa el parámetro :function. En el modulo HERRAMIETAS_GONG que tiene esta forma:
self.observarCampo = function( opciones ) {
opciones.hasOwnProperty("function") && self.ejecutarFuncion(opciones["function"]);
opciones.hasOwnProperty("url") && AJAX_GONG.updateAjax(opciones);
};
Cambios para el nuevo "observe_field"¶
Cuando pasamos una funcion javascript no hay ningun cambio en nuestro llamada al helper.
IMPORTANTE: Aunque no haya ningún tipo de cambio en la llamada al helper, evidentemente en el javascript deben eliminarse todas las referencia a prototype, y utilizar javascript "puro". En este sentido dejamos la referencia l siguiente commit donde se sustituyeron todas las referencias:
https://git.semillasl.com/gong/gor/commit/00534f94b450bcf725de6c4a986713e5aa905dbd
Cuando no pasamos una funcion (parametro function) javascript, si no que hacemos una invocacion ajax, tenemos que modificar el with.
El with pues pasa a tomar dos tipos de formas, que explicamos a continuación.
With con varios parametros.¶
Cuando se envian varios parametros o se especifica como se van a pasar los parametros:
wiith:[ { params_name: ... , value: ... } , { params_name: ... , value: ... } ]
En el código anterior teníamos javascript dentro de los parámetros que le pasábamos al helper
<%= observe_field( "proyecto_gestor_id" , :url => {:action => "libro_cambio_moneda"}, :with => "'moneda_id='+ escape($('proyecto_moneda_id').value) + '&implementador_id='+ escape($('proyecto_gestor_id').value) + '&proyecto_id=' + escape('" + (@proyecto ? @proyecto.id.to_s : "") + "')", :loading => "Element.show('spinner_proyecto')", :complete => "Element.hide('spinner_proyecto')") %>
En el nuevo helper los parametros se pasarán asi:
<%= observe_field( "presupuesto_numero_unidades", :url => {:action => :calcula_importe, :update => 'presupuesto'},
with: [{params_name: 'presupuesto[numero_unidades]',
value: 'presupuesto_numero_unidades'},
{params_name: 'presupuesto[coste_unitario_convertido]',
value: 'presupuesto_coste_unitario_convertido'}]) %>
NOTA: Como se describió en un apartado anterior no es necesario el loading y del complete
with con un solo parametros¶
Cuando solamente se le pasa un parametro en el with no es necesario cambiar nada:
<%= observe_field "gasto_partida_id", update: "subpartida",
url: {action: :subpartida} , with: "id",
loading: "Element.show('spinner'); Element.hide('botonguardar'); ",
complete: "Element.hide('spinner'); Element.show('botonguardar');" %>
<%= observe_field "gasto_partida_id", update: "subpartida",
url: {action: :subpartida} , with: "id" %>
ventana_info_popup¶
El metodo ventan_info_popup ha sido modificados introduciendo el siguiente codigo:
https://git.semillasl.com/gong/gor/blob/sin_prototype/app/helpers/application_helper.rb#L674
if no_tooltip cadena << content_tag( :script, type: "text/javascript" ) do "var iconPopup = document.getElementById(\"#{objeto_id}\"); iconPopup.addEventListener('click', function(event) { var iconPopup = document.getElementById(\"#{objeto_id}\"); event.preventDefault(); var contentPopup = document.getElementById(\"#{objeto_id}_popup\"); HERRAMIENTAS_GONG.ventanaPopup(iconPopup, contentPopup); }); ".html_safe end else cadena << content_tag( :script, type: "text/javascript" ) do "var iconPopup = document.getElementById(\"#{objeto_id}\"); var contentPopup = document.getElementById(\"#{objeto_id}_popup\"); HERRAMIENTAS_GONG.iniciarTooltip(iconPopup, contentPopup); ".html_safe end end
Existen dos posiblidades de ventana_info_popup:
- La que utiliza el widget tooltip de jquery-ui. Esta es la opcion por defecto, y varia respecto al funcionamiento anterior en que no requiere "click" sobre la "i" de información.
En HERRAMIENTAS GONG tenemos especificado el ventanaPopup:
self.iniciarTooltip = function(target, targetData){
var content;
var elementTarget = document.getElementById(target);
var isTargetDataContentElement = targetData instanceof Element;
target = typeof target !== "undefined" ? target : ".help";
if(isTargetDataContentElement) {
content = targetData.innerHTML;
} else {
targetData = typeof targetData !== "undefined" ? targetData : "title";
content = function () {
return this.getAttribute(targetData);
};
}
jQuery(target).tooltip({content: content,
position: { my: "left top", at: "right center" }});
};
- La opción en la que se pasa el parámetro no_tooltip. Esta opcion la hemos creado por que algunas ventanas popup tenía "vinculos" sobre los que se puede pinchar (ejemplo ventana info popup de gasto con el vinculo a la nota de gasto). Debido a este comportamiento se ha utilizado el widget dialog de jquery-ui.
En HERRAMIENTAS GONG tenemos especificado el ventanaPopup:
self.ventanaPopup = function(elementIconPopup, elementContentPopup) {
var iconPopup = jQuery(elementIconPopup);
var contentPopup = jQuery(elementContentPopup);
contentPopup.dialog({
autoOpen: true,
modal: true,
width:"auto",
height: "auto",
minHeight:0,
resizable: false,
position: {my: "center top", at: "center top+110", collision: "flipfit"},
open: function(event, ui) {
iconPopup.closest(".fila").effect("highlight", 8000)
jQuery(".ui-dialog-titlebar").hide();
jQuery(".ui-widget-overlay").on("click", function() {
jQuery(contentPopup).dialog("close");
});
},
});
};
ventana_popup¶
El metodo ventan_popup se ha eliminado quitando todas las referencias a las vistas que lo utilizaban. Ponemos la referencia al commit:
https://git.semillasl.com/gong/gor/commit/89f7d6e14e845cf586108786b010842a20365896
tooltip.¶
Como se puede ver en el commit:
https://git.semillasl.com/gong/gor/commit/4957a7b8ba85d6cf3aef168645fcb030b9f64114
Se elimina las referencias a Tooltip de prototype de:
- app/views/info/_listado_proyecto_detalle.html.erb
- app/views/info/_proyectos.html.erb :
- Y del Layout app/views/layouts/layout.html.erb :
Se introduce el nuevo tooltip de jquery-ui,
En HERRAMIENTAS GONG tenemos especificado el ventanaPopup:
self.iniciarTooltip = function(target, targetData){
var content;
var elementTarget = document.getElementById(target);
var isTargetDataContentElement = targetData instanceof Element;
target = typeof target !== "undefined" ? target : ".help";
if(isTargetDataContentElement) {
content = targetData.innerHTML;
} else {
targetData = typeof targetData !== "undefined" ? targetData : "title";
content = function () {
return this.getAttribute(targetData);
};
}
jQuery(target).tooltip({content: content,
position: { my: "left top", at: "right center" }});
};
Ademas dentro del ajax_gong tambien se hace una referencia al tooltip para activar tooltip tras un envio de contenidos via AJAX:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/ajax_gong.js#L40
HERRAMIENTAS_GONG.iniciarTooltip();
HERRAMIENTAS_GONG.activaSelectoresChosen();
chosen_select¶
Comos se puede ver en el commit:
https://git.semillasl.com/gong/gor/commit/11ac6eb7407cf83888945811778d07839ff40de2
Se ha eliminado la activacion del choosen del layout
<script type="text/javascript" charset="utf-8">
var activaSelectoresChosen = function() {
$$(".chosen_select").each( function(input) {
input.setAttribute("data-placeholder","<%= _('Ningún elemento seleccionado...') %>");
input.removeClassName('chosen_select');
new Chosen(input, {width: '100%', allow_single_deselect: true, include_group_label_in_selected: true, no_results_text: "<%= _('No se ha encontrado') %>" });
});
}
activaSelectoresChosen();
</script>
Y se ha sustituido por JQuery:
self.activaSelectoresChosen = function() {
var placeholder = jQuery("#selectchosen_data").data("placeholder");
var noResultText = jQuery("#selectchosen_data").data("no_results_text");
jQuery(".chosen_select").each( function(input) {
$(this).attr("data-placeholder", placeholder);
// $(this).prepend("<option value=''></option>");
// $(this).removeClass("chosen_select");
$(this).chosen({width: "100%",
allow_single_deselect: true,
include_group_label_in_selected: true,
no_results_text: noResultText,
placeholder_text_single: placeholder})
});
};
Su activacion tras una llamada ajax no es necesaria por que se ha introducido por defecto en la recepecion ajax (al igual que la activacion del tooltip):
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/ajax_gong.js#L41
HERRAMIENTAS_GONG.activaSelectoresChosen();
auto_complete¶
Hasta la fecha el autocomplete se habia implementado a traves del codigo del plugin:
https://git.semillasl.com/gong/gor/tree/gong-gor-4.01/lib/auto_complete
Incluyendo la llamada al helper en los campos de texto:
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/helpers/application_helper.rb#L98
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/helpers/application_helper.rb#L121
if otros[:autocomplete]
#cadena << text_field_with_auto_complete( objeto, atributo, {:disabled => otros[:disabled], :class => "texto" + clase}, {:method => :get, :with => "'search=' + element.value", :onchange => otros[:onchange]} )
autocomplete_with = "'search=' + element.value"
otros[:autocomplete_with_also].each do |am|
autocomplete_with += " + '&' + $('" + am + "').serialize()"
end if otros[:autocomplete_with_also]
cadena << text_field_with_auto_complete( objeto, atributo, opciones, {:method => :get, :with => autocomplete_with, :onchange => otros[:onchange], :after_update_element => otros[:after_update]} )
else
opciones[:type] = otros[:type] if otros[:type]
opciones[:name] = otros[:name] if otros[:name]
opciones[:onchange] = otros[:onchange] if otros[:onchange]
cadena << text_field( objeto, atributo, opciones)
end
Asi se ha cambiado a:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb#L186
if otros[:autocomplete]
opciones[:id] = otros[:id] || SecureRandom.hex(10)
opciones[:url] = url_for(controller: controller_name,
action: "auto_complete_for_" +
objeto + "_" + atributo)
cadena << text_field( objeto, atributo, opciones)
cadena << content_tag( :script, type: "text/javascript" ) do
"jQuery(\"##{opciones[:id]}\").click(function(event) {
event.preventDefault();
HERRAMIENTAS_GONG.textAutocomplete(#{opciones.to_json});
});".html_safe
end
else
opciones[:type] = otros[:type] if otros[:type]
opciones[:name] = otros[:name] if otros[:name]
opciones[:onchange] = otros[:onchange] if otros[:onchange]
cadena << text_field( objeto, atributo, opciones)
end
En su implementacion Javascript se ha utilizado el widget de autocomplete de jquery-ui:
self.textAutocomplete = function(opciones) {
var textField = jQuery("#" + opciones["id"]);
jQuery(textField).autocomplete({
minLength: 1,
source: function(request, response) {
jQuery.ajax({
url: opciones["url"],
type: "post",
dataType: "json",
data: {
search: request.term
},
success: function (data) {
response(data);
},
});
},
select: function (event, ui) {
event.preventDefault();
textField.val(ui.item.label)
if (opciones.after_update) {
jQuery("input[name='" + opciones.after_update.field + "']").val(ui.item.after_update);
}
},
});
jQuery(".ui-autocomplete").css("z-index", "10000");
};
Los metodos en los controladores mantendrán el nombre anterior <auto_complete_for_contolador_entidad_atributo>
def auto_complete_for_gasto_proveedor_nombre
@proveedores = Proveedor.where(activo: true, agente_id: @proyecto.implementador).where(['nombre like ?', "%#{params[:search]}%"])
render json: @proveedores.map{|p| {value: p.id, label: p.nombre, after_update: p.nif}}
end
Solo habrá que cambiar el render final que ya no utiliza el helper "auto_complete_result_3".
Cambios para el nuevo "auto_complete"¶
Todos los métodos de los controladores con los nombres <auto_complete_for_contolador_entidad_atributo> tendrán que cambiar el render final, tal y como se ha mostrado en el ejemplo anterior:
def auto_complete_for_gasto_proveedor_nombre
@proveedores = Proveedor.where(activo: true, agente_id: @proyecto.implementador).where(['nombre like ?', "%#{params[:search]}%"])
render json: @proveedores.map{|p| {value: p.id, label: p.nombre, after_update: p.nif}}
end
Hay dos posibilida de json. La opcion mas simple envia la informacion minma: value y label
def auto_complete_for_presupuesto_subpartida_nombre
if params[:presupuesto] && params[:presupuesto][:partida_id]
condiciones = ['nombre like ? and proyecto_id = ? and partida_id = ?', "%#{params[:search]}%", params[:proyecto_id].to_s, params[:presupuesto][:partida_id].to_s ]
else
condiciones = ['nombre like ? and proyecto_id = ?', "%#{params[:search]}%", params[:proyecto_id].to_s]
end
@subpartidas = Subpartida.where(condiciones).map{|s| {value: s.id, label: s.nombre}}
render json: @subpartidas
end
Esta opcion envia un json de tipo:
{value: 2633, label: "TRANSPORTE (AEREO, TERRESTRE, Y/O FLUVIAL)"}, {value: 3014, label: "PUBLICACION INVESTIGACION"}, {value: 3229, label: "INVESTIGADOR"}}
La otra opcion requiere que se haya configurado el autocomplete con la opcion after_update, y que se enviara como un parametro mas en el json final:
<%= texto _("NIF Emisor"), 'gasto', 'proveedor_nif', '1_2',
value: obj_value, autocomplete: true, disabled: disabled,
after_update: {field: "gasto[proveedor_nombre]"} %>
El controlador es el mismo del ejemplo que venimos mostrando:
def auto_complete_for_gasto_proveedor_nombre
@proveedores = Proveedor.where(activo: true, agente_id: @proyecto.implementador).where(['nombre like ?', "%#{params[:search]}%"])
render json: @proveedores.map{|p| {value: p.id, label: p.nombre, after_update: p.nif}}
end
Esta opción envía un json de tipo:
{value: 107, label: "GESTINGRAF, S.A.", after_update: "A48273312"} {value: 306, label: "UNA GESTIÓN Y COMUNICACIÓN S.COOP.", after_update: "F95688875"} {value: 559, label: "AGANDRA GESTIÓN DE SERVICIOS, S.L", after_update: "B-91890137"}
modal¶
El metodo del helper:
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/helpers/application_helper.rb#L867
# Ventana modal que pide confirmacion para el borrado de un elemento
def modal_borrado ( rotulo, url, titulo, texto, otros={} )
# Falta añadir al titulo de la ventana modal el mismo texto superior que llevan las modales sobre la variable de session.
cadena = '<div style="display:none;" id="'+ (otros[:id] || url[:id].to_s ) +'_modalconfirmar" class="elemento2_tc">'
cadena << h(titulo) + ': <br><b>'.html_safe + h(texto) + '</b><br><br>'.html_safe
cadena << '<div class="fila"><a href="#" onclick="Modalbox.hide()"> Cancelar </a> '
cadena << link_to( "Confirmar", url, :id => otros[:id].to_s + "_confirmar") unless otros[:ajax]
cadena << link_to_remote( "Confirmar", :url => url, :html => {:id => otros[:id].to_s + "_confirmar"}) if otros[:ajax]
cadena << '</b></div></div>'
cadena << "<a id=\"#{ (otros[:id] || url[:id].to_s ) }\" onclick=\"Modalbox.show($('#{ (otros[:id] || url[:id].to_s ) }_modalconfirmar'), {title: '" + h(titulo) + "', width: 600,transitions:false}); return false;\" href=\"#\" title='"+ url[:action].to_s + "'>"
cadena << h(rotulo)
cadena << "</a>"
return cadena.html_safe
end
Se le ha introducido una referencia a openModalAjax
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb#L894
opciones = {id: id,
url: url_for(url),
pretitulo: pretitulo,
titulo: titulo,
width: otros[:width] ? otros[:width].to_s : "820",
height: otros[:height] ? otros[:height].to_s : "auto"}
cadena = link_to h(rotulo), nil, :title => titulo, :id => id, :class => clase
cadena << content_tag( :script, type: "text/javascript" ) do
"document.getElementById(\"#{id}\").addEventListener('click', function(event) {
event.preventDefault();
HERRAMIENTAS_GONG.openModalAjax(#{opciones.to_json});
});".html_safe
La implementación en HERRMIANTAS_GONG esta basado en el widget dialog de jquery-ui.
self.openModalAjax = function(opciones) {
jQuery("#modal").dialog("destroy") && jQuery("#modal");
var modal = jQuery("<div id='modal'></div>");
modal.load(opciones["url"]);
modal.dialog({
autoOpen: true,
modal: true,
title: opciones["titulo"],
width: opciones["width"],
height: opciones["height"],
minHeight:0,
resizable: false,
position: {my: "center top", at: "center top+110"},
open: function(event, ui) {
jQuery(".ui-widget-overlay").on("click", function() {
modal.dialog("close");
});
jQuery("#" + opciones["id"]).closest(".fila").effect("highlight", 8000)
var widthModal = jQuery("#modal").width();
jQuery("#modal").width(widthModal + 2);
jQuery("#modal").css("text-align", "left");
jQuery(".ui-dialog-titlebar").prepend("<span class='ui-dialog-title'>" + opciones["pretitulo"] + "</span");
},
});
};
modal_borrado¶
El metodo del helper:
https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/helpers/application_helper.rb#L867
# Ventana modal que pide confirmacion para el borrado de un elemento
def modal_borrado ( rotulo, url, titulo, texto, otros={} )
# Falta añadir al titulo de la ventana modal el mismo texto superior que llevan las modales sobre la variable de session.
cadena = '<div style="display:none;" id="'+ (otros[:id] || url[:id].to_s ) +'_modalconfirmar" class="elemento2_tc">'
cadena << h(titulo) + ': <br><b>'.html_safe + h(texto) + '</b><br><br>'.html_safe
cadena << '<div class="fila"><a href="#" onclick="Modalbox.hide()"> Cancelar </a> '
cadena << link_to( "Confirmar", url, :id => otros[:id].to_s + "_confirmar") unless otros[:ajax]
cadena << link_to_remote( "Confirmar", :url => url, :html => {:id => otros[:id].to_s + "_confirmar"}) if otros[:ajax]
cadena << '</b></div></div>'
cadena << "<a id=\"#{ (otros[:id] || url[:id].to_s ) }\" onclick=\"Modalbox.show($('#{ (otros[:id] || url[:id].to_s ) }_modalconfirmar'), {title: '" + h(titulo) + "', width: 600,transitions:false}); return false;\" href=\"#\" title='"+ url[:action].to_s + "'>"
cadena << h(rotulo)
cadena << "</a>"
return cadena.html_safe
end
Se le ha introducido una referencia a openModal y a closeModal
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb#L949
cadena << content_tag( :script, type: "text/javascript" ) do
"document.getElementById(\"#{id}\").addEventListener('click', function(event) {
event.preventDefault();
HERRAMIENTAS_GONG.openModal(#{opciones.to_json});
document.getElementById(\"#{id_linkcancelar}\").addEventListener('click', function(event) {
event.preventDefault();
HERRAMIENTAS_GONG.closeModal(#{opciones.to_json});
});
});".html_safe
La implementación en HERRMIANTAS_GONG esta basado en el widget dialog de jquery-ui.
self.openModal = function(opciones) {
var modal = jQuery("#" + opciones["id"]);
modal.closest(".fila").effect("highlight", 8000)
modal.dialog({
autoOpen: true,
modal: true,
resizable: true,
title: opciones["titulo"],
width: opciones["width"],
height: opciones["height"],
minHeight:0,
resizable: false,
position: {my: "center top", at: "center top+110"},
open: function(event, ui) {
jQuery(".ui-widget-overlay").on("click", function() {
if (modal.is(":ui-dialog")) {
modal.dialog("close");
}
});
},
close: function(){
if (modal.is(":ui-dialog")) {
modal.dialog("destroy");
}
},
});
};
self.closeModal = function(opciones) {
var modal = jQuery("#" + opciones["id"]);
if (modal.is(":ui-dialog")) {
modal.dialog("close");
}
};
mensaje_cambio¶
Se ha eliminado este helper ( y el metodo en el que se apoyaba cadena_mensaje_cambio). Solo se utilizaba en application_controller.rb para el caso de no tener permisos. Ver el commit:
https://git.semillasl.com/gong/gor/commit/570c4b91a26daa06a58a3083f31e995d7c6584ae
Resumen de nuevos fiecheros, y cambios importantes¶
Existen 3 nevos ficheros importantes y que recogen y centralizan la interaccion javascript y ajax:
- heramientas_gong.js https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js
- ajax_gong.js https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/ajax_gong.js
- json_gong.rb https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/serializers/json_gong.rb
Por otro lado ha habido tambien diversos cambios en el application_helper:
https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/helpers/application_helper.rb
Chuleta de cambios.¶
Para pasar del tag sin_prototype_05 a la sustitucion en toda la rama de los cambios necesarios, dejamos en una chuleta los cambios que son necesarios:
- Controladores:
- render update > JsonGong.new
- Pasar lo RJS al controlador.
- Referencias a MB_content > actualiza_modal
- page[:identificador].value > modifica_value
- ocasionalmente: Nombre completo de los templates.
- Vistas:
- eliminar (o reformar) loading y complete.
- observe_field:
- with: modificar
- function: reescribir la función en vanilla javascript [nota-1]
[nota-1]: en principio se ha hecho una sustitución general del código pero haría falta comprobarlo: https://git.semillasl.com/gong/gor/commit/00534f94b450bcf725de6c4a986713e5aa905dbd
sin_prototype_1¶
El tag sin_prototype_1 es en el que extendemos a todo el código la propuesta de cambios que hacemos en esta documentacion y que ha quedado reflejada en el tag: sin_prototype_05.
La metodologica se basa en:
- El uso de una hoja de calculo para planificar y para llevar un checklist de todos los cambios necesarios y de su comprobación (testeo) https://docs.google.com/spreadsheets/d/1KZymL0GjCYbEh7L1E2W_Q_4G5iVm9vTgBX4jtGyMi-A/edit?usp=sharing
- La primera hoja contiene el listado de todas los menús que se van a modificar y testear.
- Para cada menu que vayamos a modificar creamos un ticket y pondremos en esta primera hoja la referencia al ticket.
- Las siguientes hojas son:
- Controladores que se deben modificar
- Vistas con lin_to_remote que se deben modificar.
- vistas con observe_field de tipo "function" que se deben modificar.
- vistas con observe_field de tipo "ajax" que se deben modificar.
- vistas que no encajan en ninguna de estas categorias.
- Menus relacionados con los plugins.
- Cada ticket que abrimos para modificar un menú del listado de la primera hoja del documento implicará cambiar algún controlador y/o vista. Buscaremos en las diferentes hojas del documento los controladores y/o vistas que se han modificado y rellenaremos su información, y concretamente le añadiremos la referencia al ticket.
- El uso de tickets.
- Se creará un ticket para cada menú que se vaya a modificar, y un ticket relacionado cuando aparezca un error que haya que resolver.
- Los tickets de menú tendrán una titulo de tipo: [Abreviatura Version] Descripción de la ruta. Un ejemplo sería: [sp1] Administración > Proyectos: https://git.semillasl.com/gong/gor/issues/38
- Los tickets de errores tendrán una referencia al ticket de la vista. Ej: [sp1] Error en observe_field (#38): https://git.semillasl.com/gong/gor/issues/37
- Tras acabar la modificación de un menú, se hará un commit con la referencia al ticket. NOTA: De forma general cada commit irá asociado a un ticket y por lo tanto a un solo menú, aunque, se puede dar casos en los que varios menús estén dentro de un mismo ticket y un mismo commit, concretamente cuando varios menús están relacionados con un mismo controlador.