J3.x

Desarrollo de un Componente MVC/Agregar AJAX

From Joomla! Documentation

< J3.x:Developing an MVC Component
This page is a translated version of the page J3.x:Developing an MVC Component/Adding AJAX and the translation is 100% complete.

Other languages:
English • ‎español • ‎français
Joomla! 
3.x
Tutorial
Desarrollo de un Componente MVC


Esta es una serie multi-artículos de tutoriales sobre cómo desarrollar un Componente Modelo-Vista-Controlador para Joomla! VersiónJoomla 3.x.

Comenzar con la Introducción, y navegar por los artículos de esta serie usando el botón de navegación en la parte inferior o en el cuadro de la derecha (los "Artículos de esta serie").



Este artículo es parte del tutorial Desarrollo de un Componente MVC para Joomla! 3.x. Te invitamos a leer las partes anteriores del tutorial antes de leer esto. Este paso introduce cómo usar AJAX dentro de un componente de Joomla.

Puede encontrar un vídeo que acompaña este paso en paso 19 agregando ajax (aunque el vídeo se grabó antes de que algunas de las cadenas de idioma estuvieran correctamente codificadas).

Functionalidad

En el paso anterior, agregamos un mapa a la página web de nuestro sitio. En este paso, extendemos esto para proporcionar una función de "Buscar aquí" para el mapa.

En la página de nuestro sitio, mostraremos un botón "Buscar aquí" y, cuando el usuario haga clic en este, el javascript realizará una llamada de Ajax al servidor para recuperar los registros de Helloworld que tienen valores de latitud y longitud que se encuentran dentro de los límites del mapa. , como se muestra actualmente en la página.

Luego mostraremos los resultados de esa búsqueda en una serie de filas debajo del mapa.

Enfoque

Iniciar la solicitud Ajax

En nuestro botón Buscar aquí pondremos un detector de clics, y en esa función javascript iniciaremos la llamada Ajax. Usaremos la llamada de la biblioteca jQuery para la solicitud Ajax, ya que eso lo hace todo bastante sencillo.

Necesitaremos obtener los límites del mapa en formato lat/long, e incluirlo en nuestra solicitud de Ajax al servidor. También estableceremos un parámetro task= para llamar a una función de controlador específica, para mantener separado nuestro código de manejo de Ajax. Y también indicaremos que esperamos que el resultado se devuelva en formato JSON.

Como la seguridad es muy importante aquí, como siempre, incluiremos un token de formulario en la página web del sitio que también enviaremos en la solicitud de Ajax.

Manejo de la solicitud Ajax

En el servidor ampliaremos nuestro código Joomla PHP y lo estructuraremos de acuerdo con el patrón MVC:

  • En nuestro controlador existente agregaremos una nueva función, cuyo objetivo principal será verificar el token de seguridad
  • Tendremos un nuevo archivo de vista que contendrá la lógica principal para reunir la respuesta JSON
  • Ampliaremos nuestro modelo existente para manejar la consulta SQL para encontrar los registros cuyos valores de latitud/longitud se ajusten a los límites de latitud/longitud del mapa.

Manejo de la Respuesta Ajax

De vuelta en nuestro código Javascript, interpretaremos el JSON resultante y generaremos los registros en algún HTML debajo del mapa.

Manejo de errores

Una parte importante de nuestro código estará asociada con el manejo de los diversos errores que pueden ocurrir. Usaremos la clase Joomla JResponseJson para ayudar tanto con la formación de la respuesta JSON como para manejar los diversos escenarios de error. Vale la pena leer la buena descripción de esta funcionalidad en Respuestas JSON con JResponseJson.

Página de Helloworld del sitio

Añadimos 3 campos dentro de una nueva sección div debajo del mapa:

  • Un botón Buscar aquí, diseñado con Bootstrap, y con un detector onclick
  • Un token de formulario de Joomla
  • Un div subsidiario que es el área donde se mostrarán los resultados de búsqueda.

site/views/helloworld/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');
?>
<h1><?php echo $this->item->greeting.(($this->item->category and $this->item->params->get('show_category'))
                                      ? (' ('.$this->item->category.')') : ''); ?>
</h1>
<?php
    $src = $this->item->imageDetails['image'];
    if ($src)
    {
        $html = '<figure>
                    <img src="%s" alt="%s" >
                    <figcaption>%s</figcaption>
                </figure>';
        $alt = $this->item->imageDetails['alt'];
        $caption = $this->item->imageDetails['caption'];
        echo sprintf($html, $src, $alt, $caption);
    } ?>
<div id="map" class="map"></div>
<div class="map-callout map-callout-bottom" id="greeting-container"></div>
<div id="searchmap">
    <?php echo '<input id="token" type="hidden" name="' . JSession::getFormToken() . '" value="1" />'; ?>
    <button type="button" class="btn btn-primary" onclick="searchHere();">
        <?php echo JText::_('COM_HELLOWORLD_SEARCH_HERE_BUTTON') ?>
    </button>
    <div id="searchresults">
    </div>
</div>

Código Javascript

En nuestro código javascript agregaremos 3 funciones:

  • el detector onclick que iniciará la llamada Ajax
  • una función para obtener los límites del mapa en formato lat/long
  • una función para mostrar los resultados.

media/js/openstreetmap.js

var map;

jQuery(document).ready(function() {
    
    // get the data passed from Joomla PHP
    // params is a Javascript object with properties for the map display: 
    // centre latitude, centre longitude and zoom, and the helloworld greeting
    const params = Joomla.getOptions('params');
    
    // We'll use OpenLayers to draw the map (http://openlayers.org/)
    
    // Openlayers uses an x,y coordinate system for positions
    // We need to convert our lat/long into an x,y pair which is relative
    // to the map projection we're using, viz Spherical Mercator WGS 84
    const x = parseFloat(params.longitude);
    const y = parseFloat(params.latitude);
    const mapCentre = ol.proj.fromLonLat([x, y]); // Spherical Mercator is assumed by default
    
    // To draw a map, Openlayers needs:
    // 1. a target HTML element into which the map is put
    // 2. a map layer, which can be eg a Vector layer with details of polygons for
    //    country boundaries, lines for roads, etc, or a Tile layer, with individual
    //    .png files for each map tile (256 by 256 pixel square).
    // 3. a view, specifying the 2D projection of the map (default Spherical Mercator),
    //    map centre coordinates and zoom level
    map = new ol.Map({
        target: 'map',
        layers: [
            new ol.layer.Tile({  // we'll get the tiles from the OSM server
                source: new ol.source.OSM()
            })
        ],
        view: new ol.View({  // default is Spherical Mercator projection
            center: mapCentre,
            zoom: params.zoom
        })
    });
    
    // Now we add a marker for our Helloworld position
    // To do that, we specify it as a Point Feature, and we add styling 
    // to define how that Feature is presented on the map
    var helloworldPoint = new ol.Feature({geometry: new ol.geom.Point(mapCentre)});
    // we'll define the style as a red 5 point star with blue edging
    const redFill = new ol.style.Fill({
        color: 'red'
    });
    const blueStroke = new ol.style.Stroke({
        color: 'blue',
        width: 3
    });
    const star = new ol.style.RegularShape({
        fill: redFill,
        stroke: blueStroke,
        points: 5,
        radius1: 20,   // outer radius of star
        radius2: 10,   // inner radius of star
    })
    helloworldPoint.setStyle(new ol.style.Style({
        image: star
    }));
    // now we add the feature to the map via a Vector source and Vector layer
    const vectorSource = new ol.source.Vector({});
    vectorSource.addFeature(helloworldPoint);
    const vector = new ol.layer.Vector({
        source: vectorSource
    });
    map.addLayer(vector);
    
    // If a user clicks on the star, then we'll show the helloworld greeting
    // The greeting will go into another HTML element, with id="greeting-container"
    // and this will be shown as an Overlay on the map
    var overlay = new ol.Overlay({
        element: document.getElementById('greeting-container'),
    });
    map.addOverlay(overlay);
        
    // Finally we add the onclick listener to display the greeting when the star is clicked
    // The way this works is that the onclick listener is attached to the map,
    // and then it works out which Feature or Features have been hit
    map.on('click', function(e) {
        let markup = '';
        let position;
        map.forEachFeatureAtPixel(e.pixel, function(feature) {  // for each Feature hit
            markup = params.greeting;
            position = feature.getGeometry().getCoordinates();
        }, {hitTolerance: 5});  // tolerance of 5 pixels
        if (markup) {
            document.getElementById('greeting-container').innerHTML = markup;
            overlay.setPosition(position);
        } else {
            overlay.setPosition();  // this hides it, if we click elsewhere
        }
    });    
});

function getMapBounds(){
    var mercatorMapbounds = map.getView().calculateExtent(map.getSize());
    var latlngMapbounds = ol.proj.transformExtent(mercatorMapbounds,'EPSG:3857','EPSG:4326');
    return { minlat: latlngMapbounds[1],
             maxlat: latlngMapbounds[3],
             minlng: latlngMapbounds[0],
             maxlng: latlngMapbounds[2] }
}
    
function searchHere() {
    var mapBounds = getMapBounds();
    var token = jQuery("#token").attr("name");
    jQuery.ajax({
        data: { [token]: "1", task: "mapsearch", format: "json", mapBounds: mapBounds },
        success: function(result, status, xhr) { displaySearchResults(result); },
        error: function() { console.log('ajax call failed'); },
    });
}

function displaySearchResults(result) {
    if (result.success) {
        var html = "";
        for (var i=0; i<result.data.length; i++) {
            html += "<p>" + result.data[i].greeting + 
                " @ " + result.data[i].latitude + 
                ", " + result.data[i].longitude + "</p>";
        }
        jQuery("#searchresults").html(html);
    } else {
        var msg = result.message;
        if ((result.messages) && (result.messages.error)) {
            for (var j=0; j<result.messages.error.length; j++) {
                msg += "<br/>" + result.messages.error[j];
            }
        }
        jQuery("#searchresults").html(msg);
    }
}

Si está utilizando Google Maps en lugar de Openlayers para mostrar el mapa, la forma de encontrar los límites del mapa se describe en esta pregunta de stackoverflow.

Código del servidor

En nuestro controlador de sitio principal incluimos la comprobación del token. Si pasa, llamamos a la función de controlador display().

site/controller.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');
/**
 * Hello World Component Controller
 *
 * @since  0.0.1
 */
class HelloWorldController extends JControllerLegacy
{
    public function mapsearch()
    {
        if (!JSession::checkToken('get')) 
        {
            echo new JResponseJson(null, JText::_('JINVALID_TOKEN'), true);
        }
        else 
        {
            parent::display();
        }
    }
}

La función estándar display() configura el modelo y la vista, y luego llama a la función display() de la vista. Sin embargo, como hemos incluido format=json como un parámetro en nuestra solicitud HTTP Ajax, Joomla buscará esta función en un archivo view.json.php en lugar del archivo view.html.php habitual. Así que ponemos nuestro código de vista en este nuevo archivo.

site/views/helloworld/view.json.php

<?php
/**
 * View file for responding to Ajax request for performing Search Here on the map
 * 
 */
 
// No direct access to this file
defined('_JEXEC') or die('Restricted access');
 
class HelloWorldViewHelloWorld extends JViewLegacy
{
	/**
	 * This display function returns in json format the Helloworld greetings
	 *   found within the latitude and longitude boundaries of the map.
	 * These bounds are provided in the parameters
	 *   minlat, minlng, maxlat, maxlng
	 */

	function display($tpl = null)
	{
		$input = JFactory::getApplication()->input;
		$mapbounds = $input->get('mapBounds', array(), 'ARRAY');
		$model = $this->getModel();
		if ($mapbounds)
		{
			$records = $model->getMapSearchResults($mapbounds);
			if ($records) 
			{
				echo new JResponseJson($records);
			}
			else
			{
				echo new JResponseJson(null, JText::_('COM_HELLOWORLD_ERROR_NO_RECORDS'), true);
			}
		}
		else 
		{
			$records = array();
			echo new JResponseJson(null, JText::_('COM_HELLOWORLD_ERROR_NO_MAP_BOUNDS'), true);
		}
	}
}

Finalmente, incluimos nuestra función getMapSearchResults en nuestro modelo existente.

site/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelItem
{
	/**
	 * @var object item
	 */
	protected $item;

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @return	void
	 * @since	2.5
	 */
	protected function populateState()
	{
		// Get the message id
		$jinput = JFactory::getApplication()->input;
		$id     = $jinput->get('id', 1, 'INT');
		$this->setState('message.id', $id);

		// Load the parameters.
		$this->setState('params', JFactory::getApplication()->getParams());
		parent::populateState();
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Get the message
	 * @return object The message to be displayed to the user
	 */
	public function getItem()
	{
		if (!isset($this->item)) 
		{
			$id    = $this->getState('message.id');
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.greeting, h.params, h.image as image, c.title as category, h.latitude as latitude, h.longitude as longitude')
				  ->from('#__helloworld as h')
				  ->leftJoin('#__categories as c ON h.catid=c.id')
				  ->where('h.id=' . (int)$id);
			$db->setQuery((string)$query);
		
			if ($this->item = $db->loadObject()) 
			{
				// Load the JSON string
				$params = new JRegistry;
				$params->loadString($this->item->params, 'JSON');
				$this->item->params = $params;

				// Merge global params with item params
				$params = clone $this->getState('params');
				$params->merge($this->item->params);
				$this->item->params = $params;

				// Convert the JSON-encoded image info into an array
				$image = new JRegistry;
				$image->loadString($this->item->image, 'JSON');
				$this->item->imageDetails = $image;
			}
		}
		return $this->item;
	}

	public function getMapParams()
	{
		if ($this->item) 
		{
			$this->mapParams = array(
				'latitude' => $this->item->latitude,
				'longitude' => $this->item->longitude,
				'zoom' => 10,
				'greeting' => $this->item->greeting
			);
			return $this->mapParams; 
		}
		else
		{
			throw new Exception('No helloworld details available for map', 500);
		}
	}

	public function getMapSearchResults($mapbounds)
	{
		try 
		{
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.greeting, h.latitude, h.longitude')
			   ->from('#__helloworld as h')
			   ->where('h.latitude > ' . $mapbounds['minlat'] . 
				' AND h.latitude < ' . $mapbounds['maxlat'] .
				' AND h.longitude > ' . $mapbounds['minlng'] .
				' AND h.longitude < ' . $mapbounds['maxlng']);
			$db->setQuery($query);
			$results = $db->loadObjectList(); 
		}
		catch (Exception $e)
		{
			$msg = $e->getMessage();
			JFactory::getApplication()->enqueueMessage($msg, 'error'); 
			$results = null;
		}

		return $results; 
	}
}

Actualización cadenas de idioma

site/language/en-GB/en-GB.com_helloworld.ini

; add new message form
COM_HELLOWORLD_LEGEND_DETAILS="New Helloworld Message Details"
COM_HELLOWORLD_HELLOWORLD_CREATING="Add message"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Sorry, you have an error"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Message details"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Please specify the greeting to add"
COM_HELLOWORLD_HELLOWORLD_GREETING_HINT="Letters only!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="Please select the associated category"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL="Reason"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC="Please say why you're adding this greeting"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT="No HTML tags!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Spam protection"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Prove you're a real person!"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Display category or not?"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="Select if you want the category displayed too"
COM_HELLOWORLD_HELLOWORLD_IMAGE_LABEL="Image information"
COM_HELLOWORLD_HELLOWORLD_PICTURE_LABEL="Image file to upload"
COM_HELLOWORLD_HELLOWORLD_PICTURE_DESC="Select the file with the image to upload"
COM_HELLOWORLD_HELLOWORLD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_CAPTION_DESC="Text to use as a caption for the image"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_DESC="Text to display if image cannot be shown"
; save and cancel confirmation messages
COM_HELLOWORLD_ADD_SUCCESSFUL="New greeting successfully saved"
COM_HELLOWORLD_ADD_CANCELLED="New greeting cancelled ok"
; file upload error conditions
COM_HELLOWORLD_ERROR_FILEUPLOAD="PHP Error %s encountered when uploading file"
COM_HELLOWORLD_ERROR_FILETOOLARGE="Upload file exceeds max size configured in Joomla"
COM_HELLOWORLD_ERROR_BADFILENAME="Upload file has an invalid filename"
COM_HELLOWORLD_ERROR_FILE_EXISTS="Upload file already exists"
COM_HELLOWORLD_ERROR_UNABLE_TO_UPLOAD_FILE="Error creating uploaded file"
; helloworld greeting page
COM_HELLOWORLD_SEARCH_HERE_BUTTON="Search here"
; Ajax handling errors
COM_HELLOWORLD_ERROR_NO_RECORDS="Didn't get any records"
COM_HELLOWORLD_ERROR_NO_MAP_BOUNDS="Error: no map bounds"

Empaquetado del componente

Contenido de su directorio de código. Cada enlace a un archivo a continuación te lleva al paso en el tutorial que tiene la última versión de ese archivo de código fuente.

helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.0" method="upgrade">

	<name>COM_HELLOWORLD</name>
	<!-- The following elements are optional and free of formatting constraints -->
	<creationDate>January 2018</creationDate>
	<author>John Doe</author>
	<authorEmail>john.doe@example.org</authorEmail>
	<authorUrl>http://www.example.org</authorUrl>
	<copyright>Copyright Info</copyright>
	<license>License Info</license>
	<!--  The version string is recorded in the components table -->
	<version>0.0.19</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_HELLOWORLD_DESCRIPTION</description>

	<!-- Runs on install/uninstall/update; New in 2.5 -->
	<scriptfile>script.php</scriptfile>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
		</sql>
	</uninstall>
	<update> <!-- Runs on update; New since J2.5 -->
		<schemas>
			<schemapath type="mysql">sql/updates/mysql</schemapath>
		</schemas>
	</update>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->
	<files folder="site">
		<filename>index.html</filename>
		<filename>helloworld.php</filename>
		<filename>controller.php</filename>
		<folder>controllers</folder>
		<folder>views</folder>
		<folder>models</folder>
	</files>

        <languages folder="site/language">
		<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
        </languages>

	<media destination="com_helloworld" folder="media">
		<filename>index.html</filename>
		<folder>images</folder>
		<folder>js</folder>
		<folder>css</folder>
	</media>

	<administration>
		<!-- Administration Menu Section -->
		<menu link='index.php?option=com_helloworld' img="../media/com_helloworld/images/tux-16x16.png">COM_HELLOWORLD_MENU</menu>
		<!-- Administration Main File Copy Section -->
		<!-- Note the folder attribute: This attribute describes the folder
			to copy FROM in the package to install therefore files copied
			in this section are copied from /admin/ in the package -->
		<files folder="admin">
			<!-- Admin Main File Copy Section -->
			<filename>index.html</filename>
			<filename>config.xml</filename>
			<filename>helloworld.php</filename>
			<filename>controller.php</filename>
			<filename>access.xml</filename>
			<!-- SQL files section -->
			<folder>sql</folder>
			<!-- tables files section -->
			<folder>tables</folder>
			<!-- models files section -->
			<folder>models</folder>
			<!-- views files section -->
			<folder>views</folder>
			<!-- controllers files section -->
			<folder>controllers</folder>
			<!-- helpers files section -->
			<folder>helpers</folder>
		</files>
		<languages folder="admin/language">
        		<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
                        <language tag="en-GB">en-GB/en-GB.com_helloworld.sys.ini</language>
		</languages>
	</administration>

</extension>

Colaboradores