You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tapestry.apache.org by Geoff Callender <ge...@gmail.com> on 2013/04/01 06:44:44 UTC

Re: Query Mobile & Tapestry

Have you tried doing it as a single page Tapestry app with each "mobile page" as a component? It's 6 months since I last worked with jquery-mobile but I think what you have to do is leave Prototype suppressed (which is tapestry5-jquery's default). Here's an example...


I<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">

<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
	<title>My App</title>

	<t:if test="productionMode">
		<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.css" />
		<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?sensor=true&amp;libraries=places"></script>
		<script src="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.js"></script>
		<script src="${context:js/jquery.ui.map.full.min.js}" type="text/javascript"></script>
	</t:if>
	<t:if test="!productionMode">
		<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.css" />
		<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?sensor=true&amp;libraries=places"></script>
		<script src="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.js"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.extensions.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.overlays.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.services.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.microdata.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.microformat.js}" type="text/javascript"></script>
		<script src="${context:js/jquery-ui-map-3.0-rc/ui/jquery.ui.map.rdfa.js}" type="text/javascript"></script>
	</t:if>
	
</head>

<body>
	<t:m.IndexPage/>
	<t:m.PersonsPage/>
	<t:m.PersonPage/>
	<!-- etc. -->
</body>

</html>


package au.com.myapp.web.pages.m;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.AssetSource;
import org.got5.tapestry5.jquery.ImportJQueryUI;

import au.com.myapp.web.annotation.PublicPage;

@PublicPage
// By specifying @ImportJQueryUI we tell tapestry5-jquery to not pull in its default theme css. 
@ImportJQueryUI(theme = "context:css/empty.css")
@Import(library = "Index.js")
public class Index {

	// Screen fields

	@Inject
	@Symbol(SymbolConstants.PRODUCTION_MODE)
	@Property
	private boolean productionMode;

	// The code
	
	public void onActivate() {
//		System.out.println("request.isXHR() = " + request.isXHR());
	}

}


...Index.js has to do one bit of trickery to do with URLs: to get from one "mobile page" to another, we generate URLs that pass parameters as query strings, but jquery-mobile doesn't want that - it wants us to provide the parameters in the page.options.pageData object - so the following code moves the parameters automatically.


// Based on https://github.com/jblas/jquery-mobile-plugins/blob/master/page-params/jqm.page.params.js .
	
(function($) {

	$(document).on("pagebeforechange", function(event, data) {
		
		// We only want to handle the case where we are being asked
		// to go to a page by URL, and only if that URL is referring
		// to an internal page by id.
	
		if (typeof data.toPage === "string") {
			var toUrlObj = $.mobile.path.parseUrl(data.toPage);
			
			if ($.mobile.path.isEmbeddedPage(toUrlObj)) {
	
				// The request is for an internal page, if the hash
				// contains query (search) params, strip them off the
				// toPage URL and then set options.dataUrl appropriately
				// so the location.hash shows the originally requested URL
				// that hash the query params in the hash.
	
				// If the hash has a query string (aka "search")
				
				var postHash = toUrlObj.hash.replace(/^#/, "");
				var postHashObj = $.mobile.path.parseUrl(postHash);

				if (postHashObj.search) {
					if (!data.options.dataUrl) {
						data.options.dataUrl = data.toPage;
					}
					data.options.pageData = queryStringToObject(postHashObj.search);
					data.toPage = toUrlObj.hrefNoHash + "#" + postHashObj.pathname;
				}
			}
		}
	});

})(jQuery);

// Given a query string, convert all the name/value pairs
// into a property/value object. If a name appears more than
// once in a query string, the value is automatically turned
// into an array.
function queryStringToObject(qstr) {
	var result = {},
		nvPairs = ( ( qstr || "" ).replace( /^\?/, "" ).split( /&/ ) ),
		i, pair, n, v;

	for ( i = 0; i < nvPairs.length; i++ ) {
		var pstr = nvPairs[ i ];
		if ( pstr ) {
			pair = pstr.split( /=/ );
			n = pair[ 0 ];
			v = pair[ 1 ];
			if ( result[ n ] === undefined ) {
				result[ n ] = v;
			} else {
				if ( typeof result[ n ] !== "object" ) {
					result[ n ] = [ result[ n ] ];
				}
				result[ n ].push( v );
			}
		}
	}

	return result;
}

// A page can use this to get the parameters passed from the previous page.
function getUrlVars(url) {
    var vars = [], hash;
    var hashes = url.slice(url.indexOf('?') + 1).split('&');
    for(var i = 0; i < hashes.length; i++)
    {
        hash = hashes[i].split('=');
        vars.push(hash[0]);
        vars[hash[0]] = hash[1];
    }
    return vars;
}


...The next page gets the parameters from the URL. Eg. in PersonPage.js:


	$(document).on("pageshow", "#"+PERSON_PAGE_ID, function(event, data) {
		urlVars = getUrlVars(window.location.href);
		var personId = urlVars.personId;
		// etc.


...This IndexPage component shows a static list of options to tap: Persons, Things, etc.:


<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">

<t:content>

	<div data-role="page" id="index">

		<div data-role="header" data-theme="b" data-position="fixed">
			<h1>My App</h1>
		</div>

		<div data-role="content">
			<ul data-role="listview" data-theme="c" data-inset="true" data-dividertheme="c">
				<li>
					<a href="#persons">Persons</a>
				</li>
				<li>
					<a href="#things">Things</a>
				</li>
				<!-- etc. -->
			</ul>
		</div>
		
	</div>	

</t:content>

</html>


package au.com.zup.web.components.m;

import org.apache.tapestry5.annotations.Import;

@Import(library = "IndexPage.js")
public class IndexPage {

}


(function($) {
	var INDEX_PAGE_ID = "index";
	
	var indexPageInited = false;	// Avoid pageinit twice (jqm bug?)

	$(document).on("mobileinit", function() {
		$.mobile.touchOverflowEnabled = true;
	});

	$(document).on("pageinit", "#"+INDEX_PAGE_ID, function(event, data) {
		
		if (!indexPageInited) {
			initThis();
			initThat();
			indexPageInited = false;
		}

	});
	
	$(document).on("pageshow", "#"+INDEX_PAGE_ID, function(event, data) {
		// Populate screen fields here...

	});

})(jQuery);


...In the PersonsPage component you might want to list persons, from the database. PersonsPage.tml could have an empty content div: 


	<script type="text/javascript">
		// We do this here, not in Tapestry.Initializer, because on page refresh Tapestry.Initializer runs AFTER the jqm page events. 
		var getPersonsUrl = "${getPersonsUrl}";
	</script>

	<!-- ...snipped... -->

		<div data-role="content" id="personsContent"> 
		</div>

	<!-- ...snipped... -->


...and the PersonsPage.js  could request the data (AJAX) and render it:


	function requestPersons(aFilterValue) {
		$.mobile.showPageLoadingMsg(); 

		var url = getPersonsUrl;
		url += "?filter=" + aFilterValue;
		var parameters = {};

		$.ajax({
			url: url, 
			data: parameters,
			dataType: "json",
			success: function(personsData) {
				renderPersonsContent(personsData);
				$.mobile.hidePageLoadingMsg(); 
			}
		});

	}
	
	function renderPersonsContent(personsData) {

		var $content = resolve("#"+PERSONS_CONTENT_ID);
		$content.html("");
		
		if (personsData.persons) {
			var persons = personsData.persons;
			
			var block = '';
			block += '<ul id="personsList" data-role="listview" data-theme="c" data-inset="false" data-dividertheme="c" data-filter="true" data-filter-theme="d">';
			$.each(persons, function(index) {
				var person = persons[index];

				block += '<li class="personRow">';
				{
					block += '<a href="#person?personId=' + person.pid + '">';
					{
						block += '<p>' + person.pnm + '</p>';
					}
					block += '</a>';
				}
				block += '</li>';
			}

			block += '</ul>';
			
			$content.append(block);
		}
		else {
			$content.append('<h4>Persons not found.</h4>');
		}

		var $page = resolve("#"+PERSONS_PAGE_ID);
		$page.page();
		$page.trigger("create");
		
	}


...and PersonsPage.java can handle the AJAX request:


	private static final String GET_PERSONS = "GetPersons";

	public String getGetPersonsUrl() {
		Link link = componentResources.createEventLink(GET_PERSONS);
		return link.toAbsoluteURI();
	}

	@OnEvent(value = GET_PERSONS)
	Object onGetPersons() {

		if (!request.isXHR()) {
			return Index.class;
		}

		List<Person> persons = null;

		try {
			aFilterValue = request.getParameter("filter");
			persons = personFinderService.findPersons(aFilterValue);
		}
		catch (Exception e) {
			// Ignore - defaults will be used
		}

		JSONObject personsData = new JSONObject();

		if (person != null) {
			JSONArray personsArray = new JSONArray();

			for (Person person : persons) {
				JSONObject personData = new JSONObject();
				personData.put("pid", person.getId());
				personData.put("pnm", person.getName());
				personsArray.put(personData);
			}

			// Put the array in the JSON object to return - TODO maybe we can return the array instead

			personsData.put("persons", personsArray);
		}

		return personsData;
	}


Hope this helps,

Geoff


On 31/03/2013, at 10:07 AM, Alexander Sommer wrote:

> Hi!
> 
> I am trying to build a mobile web app based on jquery mobile. For that
> reason, I need to reuse my services built with tapestry - which turned out
> not being as trivial as expected.
> 
> I was reading in the forum (eg
> http://mail-archives.apache.org/mod_mbox/tapestry-users/201201.mbox/%3Ccd6ed1fa-1e5c-4cbb-a6e9-67eeb842ed42@mail-nic-00.intern.albourne.com%3E),
> that jquery is complicating (correct me if this is not true!!) things due
> to prototype used in tapestry. For that reason, I would like to try a 100%
> clean approach - my web app client should not be depended on tapestry
> (GUI!) at all, I just want to use my tapestry services.
> 
> one service for example is:
> 
> http://www.airwriting.com/mobile/public:getLocalGroups?lat=48.15&lng=16.3
> 
> which is returning valid json. (check for non believers http://jsonlint.com/
> )
> 
> BUT
> 
> if I want to use this service, I run into the crossDomain problem with
> javascript, because my mobile webapp for example is (for testing purposes)
> hosted here: http://www.learnclip.com/airwritingweb/ (so not on
> airwriting.com)
> 
> Now, I see two options to solve this crossDomain issue;
> 
> *A.*
> Run the web app build with jquery on the same domain. -> how can I do this?
> Is it possible to copy the whole web app project just to some tapestry
> folder and tell tapestry that it should "ignore" that folder - but still
> making it accessable from the outside? eg. via
> www.airwriting.com/mobile/jquery ?
> 
> *B.*
> if i got it right (http://api.jquery.com/jQuery.getJSON/), the function
> 
> $.getJSON in
> 
> jquery mobile needs  a ?jsoncallback=? parameter, in my case
> 
> http://www.airwriting.com/mobile/public:getLocalGroups?lat=48.15&lng=16.3?jsoncallback=
> ?
> 
> but I really have no idea how I have to handle the jsoncallback in my
> service method:
> 
> private final String CONTENT_TYPE = "application/javascript";
> 
>    public StreamResponse onGetLocalGroups(){
>        Double latitude = Double.parseDouble(request.getParameter("lat"));
>        Double longitude = Double.parseDouble(request.getParameter("lng"));
> 
>        List<Group> groups = groupService.getGroupsOnLocation(new
> GeoPoint(latitude, longitude).toPoint(), limit, offset);
> 
>        JSONArray jsGroups = new JSONArray();
>        for( Group group : groups ){
>            jsGroups.put(group.toJSON());
>        }
>        return new TextStreamResponse(CONTENT_TYPE, jsGroups.toString());
>    }
> <http://www.airwriting.com/mobile/public:getLocalGroups?lat=48.15&lng=16.3>
> 
> just calling (from a not airwriting.com domain)
> 
> <script>
> (function() {
>  var url = "
> http://www.airwriting.com/mobile/public:getLocalGroups?lat=48.15&lng=16.3";
>  $.getJSON( url, {
>    format: "json"
>  })
>  .done(function( data ) {
>    $.each( data.items, function( i, item ) {
> 
>       alert (item);
> 
>    });
>  });
> })();
> </script>
> 
> 
> 
> DOES NOT WORK - so i guess it has something to do with the callback.
> Reading me deeper into the jsonP theory, my understanding focuses on the
> need for writing somekind of filter in the filterrequestchain for handling
> this kind of crossDomain request. puh.. (
> http://code.google.com/p/jsonp-java/)
> 
> 
> any help is highly appreciated.. thx,
> alex
> 
> 
> further, good links:
> http://json-p.org/
> http://alotaiba.github.com/FlyJSONP/#!/demo