Project

General

Profile

Eliminar prototype

DOCUMENTO EN CONSTRUCCIÓN

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:

https://git.semillasl.com/gong/gor/tree/sin_prototype_05/app/controllers/gasto_proyecto_controller.rb#L522

     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:

https://git.semillasl.com/gong/gor/blob/tree/gong-gor-4.01/app/controllers/datos_proyecto_controller.rb#L240

     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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/datos_proyecto_controller.rb#L240

      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:

https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/controllers/presupuesto_proyectos_controller.rb#L372

        page[:presupuesto_importe].value = numero_unidades * coste_unitario if numero_unidades && coste_unitario

Se sustituyen por:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/presupuesto_proyectos_controller.rb#L372

        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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L163

    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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L30

    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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L30

    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

https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/views/presupuesto_proyectos/_formulario.html.erb#L19

  <%= 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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/views/presupuesto_proyectos/_formulario.html.erb#L19

      <%= 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:

https://git.semillasl.com/gong/gor/blob/gong-gor-4.01/app/views/gasto_proyectos/_formulario.html.erb#14

      <%= 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');" %>

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/views/gasto_proyectos/_formulario.html.erb#14

      <%= 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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L47

    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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L80

    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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L47

    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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascripts/herramientas_gong.js#L32

    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>

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/gasto_proyectos_controller.rb#L775

  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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/gasto_proyectos_controller.rb#L775

  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:

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/controllers/gasto_proyectos_controller.rb#L775

  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> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; '
    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.

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascript/herramientas_gong.js#L136

    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> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; '
    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.

https://git.semillasl.com/gong/gor/blob/sin_prototype_05/app/assets/javascript/herramientas_gong.js#L101

     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:

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.
Financiado por:

Desarrollado por:
Software libre forjado en: