Blog

Getting a native App feeling with HTML5, CSS3 and Javascript, Part 3

In this third part of how to get a native app feeling with HTML, Javascript and CSS we will build a basic but solid navigation system. This is where we start building our application and influence our know-how from part 1 and part 2.

While my colleague at Point Software, François Scheurer, is working on his next article of how to make a chess game in the Scala programming language, we will use our know-how gained in the last two parts of this series to build the frontend of the game. The whole logic will stay on the server so that we can focus only on the appearance and user interface of the app.

Lets start with a clean and minimal HTML5 site in index.html. I already added the script and style dependencies to the head. We will create them later in the assets/ folder.

<!DOCTYPE html>
<html>
	<head>
	<meta charset="UTF-8">
	<title>My App</title>
	
	<link rel="stylesheet" href="assets/reset.css">
	
	<meta name="viewport" content="user-scalable=0, initial-scale=1.0, width=device-width" />
	<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

	<meta name="apple-mobile-web-app-capable" content="yes" />
	
	<script type="text/javascript" src="assets/scripts.js"></script>
	
	<link href='http://fonts.googleapis.com/css?family=Lato:400,900' rel='stylesheet' type='text/css'>
	<link rel="stylesheet" href="assets/style.css">
</head>

<body>

<div id="main">
	
	<div id="toolbar">
	<h1>Chess</h1>
	</div>
	
	<div id="content"></div>
	
</div>

</body>

</html>

All I have done is adding the snippets from part 1 of this series to the minimal HTML construct.

In order to make the small project more clearly arranged, I wrote a relatively complex but useful script, which allows further options to add and remove events based on the two functions of part 2 in this series.

The add function now takes a parameter more to group the events. The remove function now takes a group parameter as well as an optional node and event parameter to target specific events, nodes or whole groups. This is very useful as it is almost impossible to remove all active events for a screen. When DOM elements get removed while navigating through our app, events still exist in the background causing memory leaks or strange behaviors. With those two functions we can now group our events corresponding to the active screen. As a user navigates through our app we can target our defined group and remove those events very easily.

To initialize all of our scripts as the DOM is loaded we add this snippet:

var onReady = function(init) {
	document.addEventListener("DOMContentLoaded", init, false);
}

And here the aforementioned event management functions:

var __eventHandlers__ = {};

var isSet = function (arg) {
	return typeof arg !== 'undefined';
}

var addEvent = function (group, node, event, handler, capture) {
	if (!isSet(capture)) {
		capture = false;
	}
	
	var nodeId = node.id;
	
	if (!(group in __eventHandlers__)) {
		// _eventHandlers stores references to groups
		__eventHandlers__[group] = {};
	}
	if (!(nodeId in __eventHandlers__[group])) {
		// _eventHandlers stores references to nodes
		__eventHandlers__[group][nodeId] = {ref : node};
	}
	if (!(event in __eventHandlers__[group][nodeId])) {
		// each entry contains another entry for each event type
		__eventHandlers__[group][nodeId][event] = [];
	}
	// capture reference
	__eventHandlers__[group][nodeId][event].push([handler, capture]); //register handler
	
	node.addEventListener(event, handler, capture);
}

var removeEvent = function (group, node, event) {
	if (group in __eventHandlers__) {
		
		var nodes = __eventHandlers__[group];
		
		var nodesObj = {};
		
		// if node is set use it, otherwise use all nodes for the given group
		if (isSet(node)) {
			nodesObj[node.id] = nodes[node.id];
		} else {
			nodesObj = nodes;
		}
			
		for (var n in nodesObj) { 

			var eventsObj = {};
			
			// if event is set use it, otherwise use all events for the given node
			if (isSet(event)) {
				eventsObj[event] = nodesObj[n][event];
			} else {
				eventsObj = nodesObj[n];
			}
			
			for (var e in eventsObj) {
				if (e !== 'ref') {
					console.log(e);
				
					var eventHandlers = nodes[n][e];
					
					// iterate through all cached handlers of an event
					for (var h = eventHandlers.length; h--;) {
						var handler = eventHandlers[h];
						
						nodes[n]['ref'].removeEventListener(e, handler[0], handler[1]); // remove the event
					}
					
					// delete the event obj	
					delete nodes[n][e];
					
					// delete the node obj if empty (1 because of the ref attribute)
					if (Object.keys(nodes[n]).length === 1) {
						delete nodes[n];
					}
				}
			}
		}

		// delete the group obj if empty
		if (Object.keys(nodes).length === 0) {
			delete __eventHandlers__[group];
		}			
	}
}

The addEvent function takes a group, the HTML element, a handler function and a boolean for capture (Event capture is explained here).

We will leave the script file open and append the onTap function from part two to the file as well as a load function to load HTML templates as we navigate through our app.

Element.prototype.onTap = function(group, handler, useCapture) {
	var elem = this;
	var moved = false;
	var useCapture = useCapture || false;
	var t1, t2;
	
	var isTouchDevice = (function () {
		return "ontouchstart" in window // works on most browsers
			|| "onmsgesturechange" in window; // works on ie10
	})();
	
	if (!isTouchDevice) {
		addEvent(group, elem, "click", handler, useCapture);
		return;
	}
	
	addEvent(group, elem, "touchstart", function(e) {
		console.log('touchstart');
		t1 = new Date().getTime();
		
		if (moved) {
        	e.preventDefault();
        }
		elem.className = 'hover';
		
        moved = false;
	}, useCapture);
	
	addEvent(group, elem, "touchend", function(e) {
		console.log('touchend');
		
		t2 = new Date().getTime();

		if ((t2 - t1) > 200) {
			moved = true;
		}
		
		elem.className = '';
				
		e.preventDefault();
		
		if (!moved) {
			setTimeout(function() {
				handler(e, elem);
			}, 1);
		}
        moved = false;
	}, useCapture);
}

Element.prototype.load = function ( url, callback, notReady ) {	
	if (window.XMLHttpRequest) {
		var xmlhttp = new XMLHttpRequest(); // code for IE7+, Firefox, Chrome, Opera, Safari
	}
	else {
		var xmlhttp = new ActiveXObject( "Microsoft.XMLHTTP" ); // code for IE6, IE5
	}
	
	var that = this;
	
	xmlhttp.open('GET', url, true);
	xmlhttp.onreadystatechange = function() {
		if (xmlhttp.readyState != 4) {
			if (isSet(notReady))
				notReady();
		}
		if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
			that.innerHTML = xmlhttp.responseText;
			if (isSet(callback))
				callback();
		}
	}
	xmlhttp.send();
}

Once those steps are made we can continue by building the HTML and CSS for our application. There are also a few things to consider like working a lot with percentual values. This will ensure that our app can be viewed on bigger or smaller screens too. For CSS3 styles we also have to use some vendor prefixes in order to make them work properly in all browsers.

I won’t explain the CSS as it is pretty basic:

body {
	color:#000;
	background-color:#fff;
}

#main {
	position:absolute;
	top:0;
	right:0;
	bottom:0;
	left:0;
	overflow:hidden;
	font-family:"Lato", Verdana, Geneva, sans-serif;
	background:#f8f8f8;
}

#toolbar {
	position:fixed;
	top:0px;
	height:60px;
	width:100%;
	
	background: #fff;
	border-bottom:1px solid #ddd;
}

#toolbar h1 {
	width:100%;
	padding-top:22px;
	text-align:center;
	font-size:18px;
	color:#222;
	font-weight:400;
}

#content {
	position:absolute;
	top:60px;
	width:100%;
	font-size:14px;
	font-weight:400;
}

ul.simple-list {
	position:relative;
	float:left;
	width:100%;
	margin-top:40px;
	border-top:1px solid #ddd;
	border-bottom:1px solid #ddd;
	background-color:#fff;
}

ul.simple-list li {
	width:100%;
	border-bottom:1px solid #eee;
	height:46px;
}

ul.simple-list li:last-of-type {
	border-bottom:0px solid #eee;
}

ul.simple-list li.hover {
	background:#eee;
}

ul.simple-list li .text {
	position:absolute;
	margin:15px 0;
	padding-left:50px;
}

ul.simple-list li .icon {
	position:absolute;
	left:6px;
	margin-top:6px;
	padding-left:2px;
	padding-right:2px;
	-webkit-border-radius: 3px;
	-moz-border-radius: 3px;
	border-radius: 3px;
}

ul.simple-list li .icon img {
	margin-top:2px;
	height:30px;
}



#chess-board {
	position:absolute;
	width:304px;
	height:304px;
	top:140px;
	left:8px;
	border:1px solid #ccc;
}

#chess-board .bright {
	position:relative;
	float:left;
	width:38px;
	height:38px;
	background:#f8f8f8;
}

#chess-board .dark {
	position:relative;
	float:left;
	width:38px;
	height:38px;
	background:#333;
}

You will need the images provided in the zip file at the bottom of this article. Now enough with all those setups! Let us start with the navigation logic. For our small app it is very simple. One starting screen or menu, and the actual game screen. For those two screens we create now start.html and chess.html. These are our templates which we load through our load function.

start.html:

<ul class="simple-list">
	<li id="play-game"><span class="icon" style="background:#F33;"><img src="assets/play_icon.png"></span><span class="text">Play</span></li>
</ul>

chess.html:

<ul class="simple-list">
	<li id="back-btn"><span class="icon" style="background:#3C3;"><img src="assets/home_icon.png"></span><span class="text">Back</span></li>
</ul>

<div id="chess-board"></div>

And this is what our navigation logic looks like:

// register element variables
var playBtn,
	backBtn,
	contentElm;

var start = function() {
	removeEvent('board');
	
	contentElm = document.getElementById('content');
	
	contentElm.load('start.html', function() {
		playBtn = document.getElementById('play-game');
		
		playBtn.onTap('start', startGame);
	});
}

var startGame = function() {
	removeEvent('start');
	
	contentElm.load('chess.html', function() {
		backBtn = document.getElementById('back-btn');
		backBtn.onTap('board', start);
	
		var board = document.getElementById('chess-board');
		var fields =  '';
		var clazz;
		
		for (var i=0;i<8;i++) {
			for (var j=0;j<8;j++) {
				clazz = ((i + j) % 2 === 0) ? 'dark' : 'bright';
				
				fields += '<div id="field_' + i + '_' + j + '" class="' + clazz + '"></div>';
			}
		}
		
		board.innerHTML = fields;
	});
}

onReady(start);

Now we have a simple navigation. With our removeEvent function it is now very confortable to remove events which’s nodes are not in the DOM anymore.

Tipp: if you log __eventHandlers__ you get a list of all registered and active events, as long as they were registered with the addEvent function.

That navigation is very simple though, but it can also be done that way in a more complex navigation.

In the next part, we try to connect to the chess-engine via Websockets and we will also take a look at animations.

I hope you had a good reading and your feedback is always appreciated!

Download the .zip of this project: Download

Kommentare

  1. Hi!

    Thanks very much for an in-depth look at staging a web app for mobile. I will take heed to all the tips you have provided. I do appreciate the evenHandler registration that you have made. I worry about my code getting too unorganized when using plain javascript. Your 3-part series is very informative.

    All the best from SF, Isa

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *