Tag Archiv: learn
We power users love our keyboard shortcuts. We use them everywhere – in our code editor, in Photoshop, in gmail. And we hate it when we hit Ctrl+S in a web app, only to see our browser offering to download it. Adding shortcuts to your application is not hard at all. In this quick tip, we will show you how to do it, by using Mousetrap.js.
1. Single Keys
Single keys are easy. You can do it with a simple event listener for keypress on the document object. But with Mousetrap, it is even better.
After hitting the run button, focus the editor by clicking on it. Otherwise, it won’t register your key presses and the demo won’t work.
(Play with our code editor on Tutorialzine.com)
2. Alternative Symbols
Mousetrap shines when listening for more complex key combinations like capital letters and special symbols.
(Play with our code editor on Tutorialzine.com)
3. Key combinations
Combinations that involve the Control key are equally easy (see the next example for how to listen for both Control and the OS X Command key).
(Play with our code editor on Tutorialzine.com)
4. Multiple Combinations
Passing an array instead of a string lets you listen for multiple key combinations at once. This is useful when you have to listen for combinations which involve the Control (for Windows and Linux) and Command (for Mac) keys.
(Play with our code editor on Tutorialzine.com)
5. Sequences
This type of shortcuts are very powerful, and are used in apps like gmail. Works with array keys as well!
(Play with our code editor on Tutorialzine.com)
Conclusion
This was our quick tip on keyboard shortcuts. If you’ve used keyboard hotkeys before, or are brave enough to experiment with them in your next project, do share the results with us in the comments below.
The idea behind single page applications (SPA) is to create a smooth browsing experience like the one found in native desktop apps. All of the necessary code for the page is loaded only once and its content gets changed dynamically through JavaScript. If everything is done right the page shouldn’t ever reload, unless the user refreshes it manually.
There are many frameworks for single page applications out there. First we had Backbone, then Angular, now React. It takes a lot of work to constantly learn and re-learn things (not to mention having to support old code you’ve written in a long forgotten framework). In some situations, like when your app idea isn’t too complex, it is actually not that hard to create a single page app without using any external frameworks. Here is how to do it.
Note: To run this example after downloading it, you need a locally running webserver like Apache. Our demo uses AJAX so it will not work if you simply double-click index.html for security reasons.
The Idea
We will not be using a framework, but we will be using two libraries – jQuery for DOM manipulation and event handling, and Handlebars for templates. You can easily omit these if you wish to be even more minimal, but we will use them for the productivity gains they provide. They will be here long after the hip client-side framework of the day is forgotten.
The app that we will be building fetches product data from a JSON file, and displays it by rendering a grid of products with Handlebars. After the initial load, our app will stay on the same URL and listen for changes to the hash part with the hashchange event. To navigate around the app, we will simply change the hash. This has the added benefit that browser history will just work without extra effort on our part.
The Setup
Our project’s folder
As you can see there isn’t much in our project folder. We have the regular web app setup – HTML, JavaScript and CSS files, accompanied by a products.json containing data about the products in our shop and a folder with images of the products.
The Products JSON
The .json file is used to store data about each product for our SPA. This file can easily be replaced by a server-side script to fetch data from a real database.
products.json
[
{
"id": 1,
"name": "Sony Xperia Z3",
"price": 899,
"specs": {
"manufacturer": "Sony",
"storage": 16,
"os": "Android",
"camera": 15
},
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
"rating": 4,
"image": {
"small": "/images/sony-xperia-z3.jpg",
"large": "/images/sony-xperia-z3-large.jpg"
}
},
{
"id": 2,
"name": "Iphone 6",
"price": 899,
"specs": {
"manufacturer": "Apple",
"storage": 16,
"os": "iOS",
"camera": 8
},
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
"rating": 4,
"image": {
"small": "/images/iphone6.jpg",
"large": "/images/iphone6-large.jpg"
}
}
]
The HTML
In our html file we have several divs sharing the same class “page”. Those are the different pages (or as they are called in SPA – states) our app can show. However, on page load all of these are hidden via CSS and need the JavaScript to show them. The idea is that only one page can be visible at a time and our script is the one to decide which one it is.
index.html
<div class="main-content">
<div class="all-products page">
<h3>Our products</h3>
<div class="filters">
<form>
Checkboxes here
</form>
</div>
<ul class="products-list">
<script id="products-template" type="x-handlebars-template">
{{#each this}}
<li data-index="{{id}}">
<a href="#" class="product-photo"><img src="{{image.small}}" height="130" alt="{{name}}"/></a>
<h2><a href="#"> {{name}} </a></h2>
<ul class="product-description">
<li><span>Manufacturer: </span>{{specs.manufacturer}}</li>
<li><span>Storage: </span>{{specs.storage}} GB</li>
<li><span>OS: </span>{{specs.os}}</li>
<li><span>Camera: </span>{{specs.camera}} Mpx</li>
</ul>
<button>Buy Now!</button>
<p class="product-price">{{price}}$</p>
<div class="highlight"></div>
</li>
{{/each}}
</script>
</ul>
</div>
<div class="single-product page">
<div class="overlay"></div>
<div class="preview-large">
<h3>Single product view</h3>
<img src=""/>
<p></p>
<span class="close">×</span>
</div>
</div>
<div class="error page">
<h3>Sorry, something went wrong :(</h3>
</div>
</div>
We have three pages: all-products (the product listing), single-product (the individual product page) and error.
The all-products page consists of a title, a form containing checkboxes for filtering and a <ul> tag with the class “products-list”. This list is generated with handlebars using the data stored in products.json, creating a <li> for each entry in the json. Here is the result:
The Products
Single-product is used to show information about only one product. It is empty and hidden on page load. When the appropriate hash address is reached, it is populated with product data and shown.
The error page consist of only an error message to let you know when you’ve reached a faulty address.
The JavaScript Code
First, lets make a quick preview of the functions and what they do.
script.js
$(function () {
checkboxes.click(function () {
// The checkboxes in our app serve the purpose of filters.
// Here on every click we add or remove filtering criteria from a filters object.
// Then we call this function which writes the filtering criteria in the url hash.
createQueryHash(filters);
});
$.getJSON( "products.json", function( data ) {
// Get data about our products from products.json.
// Call a function that will turn that data into HTML.
generateAllProductsHTML(data);
// Manually trigger a hashchange to start the app.
$(window).trigger('hashchange');
});
$(window).on('hashchange', function(){
// On every hash change the render function is called with the new hash.
// This is how the navigation of our app happens.
render(window.location.hash);
});
function render(url) {
// This function decides what type of page to show
// depending on the current url hash value.
}
function generateAllProductsHTML(data){
// Uses Handlebars to create a list of products using the provided data.
// This function is called only once on page load.
}
function renderProductsPage(data){
// Hides and shows products in the All Products Page depending on the data it recieves.
}
function renderSingleProductPage(index, data){
// Shows the Single Product Page with appropriate data.
}
function renderFilterResults(filters, products){
// Crates an object with filtered products and passes it to renderProductsPage.
renderProductsPage(results);
}
function renderErrorPage(){
// Shows the error page.
}
function createQueryHash(filters){
// Get the filters object, turn it into a string and write it into the hash.
}
});
Remember that the concept of SPA is to not have any loads going on while the app is running. That’s why after the initial page load we want to stay on the same page, where everything we need has already been fetched by the server.
However, we still want to be able to go somewhere in the app and, for example, copy the url and send it to a friend. If we never change the app’s address they will just get the app the way it looks in the beginning, not what you wanted to share with them. To solve this problem we write information about the state of the app in the url as #hash. Hashes don’t cause the page to reload and are easily accessible and manipulated.
On every hashchange we call this:
function render(url) {
// Get the keyword from the url.
var temp = url.split('/')[0];
// Hide whatever page is currently shown.
$('.main-content .page').removeClass('visible');
var map = {
// The Homepage.
'': function() {
// Clear the filters object, uncheck all checkboxes, show all the products
filters = {};
checkboxes.prop('checked',false);
renderProductsPage(products);
},
// Single Products page.
'#product': function() {
// Get the index of which product we want to show and call the appropriate function.
var index = url.split('#product/')[1].trim();
renderSingleProductPage(index, products);
},
// Page with filtered products
'#filter': function() {
// Grab the string after the '#filter/' keyword. Call the filtering function.
url = url.split('#filter/')[1].trim();
// Try and parse the filters object from the query string.
try {
filters = JSON.parse(url);
}
// If it isn't a valid json, go back to homepage ( the rest of the code won't be executed ).
catch(err) {
window.location.hash = '#';
}
renderFilterResults(filters, products);
}
};
// Execute the needed function depending on the url keyword (stored in temp).
if(map[temp]){
map[temp]();
}
// If the keyword isn't listed in the above - render the error page.
else {
renderErrorPage();
}
}
This function takes into consideration the beginning string of our hash, decides what page needs to be shown and calls the according functions.
For example if the hash is ‘#filter/{“storage”:[“16″],”camera”:[“5″]}’, our codeword is ‘#filter’. Now the render function knows we want to see a page with the filtered products list and will navigate us to it. The rest of the hash will be parsed into an object and a page with the filtered products will be shown, changing the state of the app.
This is called only once on start up and turns our JSON into actual HTML5 content via handlebars.
function generateAllProductsHTML(data){
var list = $('.all-products .products-list');
var theTemplateScript = $("#products-template").html();
//Compile the template
var theTemplate = Handlebars.compile (theTemplateScript);
list.append (theTemplate(data));
// Each products has a data-index attribute.
// On click change the url hash to open up a preview for this product only.
// Remember: every hashchange triggers the render function.
list.find('li').on('click', function (e) {
e.preventDefault();
var productIndex = $(this).data('index');
window.location.hash = 'product/' + productIndex;
})
}
This function receives an object containing only those products we want to show and displays them.
function renderProductsPage(data){
var page = $('.all-products'),
allProducts = $('.all-products .products-list > li');
// Hide all the products in the products list.
allProducts.addClass('hidden');
// Iterate over all of the products.
// If their ID is somewhere in the data object remove the hidden class to reveal them.
allProducts.each(function () {
var that = $(this);
data.forEach(function (item) {
if(that.data('index') == item.id){
that.removeClass('hidden');
}
});
});
// Show the page itself.
// (the render function hides all pages so we need to show the one we want).
page.addClass('visible');
}
Shows the single product preview page:
function renderSingleProductPage(index, data){
var page = $('.single-product'),
container = $('.preview-large');
// Find the wanted product by iterating the data object and searching for the chosen index.
if(data.length){
data.forEach(function (item) {
if(item.id == index){
// Populate '.preview-large' with the chosen product's data.
container.find('h3').text(item.name);
container.find('img').attr('src', item.image.large);
container.find('p').text(item.description);
}
});
}
// Show the page.
page.addClass('visible');
}
Takes all the products, filters them based on our query and returns an object with the results.
function renderFilterResults(filters, products){
// This array contains all the possible filter criteria.
var criteria = ['manufacturer','storage','os','camera'],
results = [],
isFiltered = false;
// Uncheck all the checkboxes.
// We will be checking them again one by one.
checkboxes.prop('checked', false);
criteria.forEach(function (c) {
// Check if each of the possible filter criteria is actually in the filters object.
if(filters[c] && filters[c].length){
// After we've filtered the products once, we want to keep filtering them.
// That's why we make the object we search in (products) to equal the one with the results.
// Then the results array is cleared, so it can be filled with the newly filtered data.
if(isFiltered){
products = results;
results = [];
}
// In these nested 'for loops' we will iterate over the filters and the products
// and check if they contain the same values (the ones we are filtering by).
// Iterate over the entries inside filters.criteria (remember each criteria contains an array).
filters[c].forEach(function (filter) {
// Iterate over the products.
products.forEach(function (item){
// If the product has the same specification value as the one in the filter
// push it inside the results array and mark the isFiltered flag true.
if(typeof item.specs[c] == 'number'){
if(item.specs[c] == filter){
results.push(item);
isFiltered = true;
}
}
if(typeof item.specs[c] == 'string'){
if(item.specs[c].toLowerCase().indexOf(filter) != -1){
results.push(item);
isFiltered = true;
}
}
});
// Here we can make the checkboxes representing the filters true,
// keeping the app up to date.
if(c && filter){
$('input[name='+c+'][value='+filter+']').prop('checked',true);
}
});
}
});
// Call the renderProductsPage.
// As it's argument give the object with filtered products.
renderProductsPage(results);
}
Shows the error state:
function renderErrorPage(){
var page = $('.error');
page.addClass('visible');
}
Stringifies the filters object and writes it into the hash.
function createQueryHash(filters){
// Here we check if filters isn't empty.
if(!$.isEmptyObject(filters)){
// Stringify the object via JSON.stringify and write it after the '#filter' keyword.
window.location.hash = '#filter/' + JSON.stringify(filters);
}
else{
// If it's empty change the hash to '#' (the homepage).
window.location.hash = '#';
}
}
Conclusion
Single page applications are perfect when you want give your project a more dynamic and fluid feel, and with the help of some clever design choices you can offer your visitors a polished, pleasant experience.
In this tutorial, we are going to build a shout box with PHP and jQuery, which allows visitors of your website to leave short comments to one another. Shouts will be stored on the server as files, no database like MySQL will be required. We are going to use two PHP libraries to make things easier – Flywheel for storing the shouts as json files and RelativeTime for creating human readable relative time stamps. We will be using Composer to install these libraries.
On the client side, we are using plain jQuery code, and the Emoji One library, which is a free project and library for adding pretty emojis to web apps. Let’s begin!
Running the shoutbox
You can grab the source code from the download button above. It has plenty of comments and is easy to follow. To run it, simply upload it to your web hosting space or add it to the apache htdocs folder if you run something like XAMPP or MAMP. Then, open http://localhost in your browser (or your website, if you uploaded it to your hosting space). Here are a few things to look for:
- The zip files already contains the dependencies, so you don’t need to install Composer. This makes it easy to get started with the code – simply upload it and use it!
- Make sure that the data/shouts directory exists and is writable. Otherwise you will see errors in your log file and no shouts will be stored. You might need to chmod it to 777 if you keep seeing errors.
The HTML
Let’s start with index.html
. It is a regular HTML5 document, which includes our JavaScript libraries, scripts and stylesheets. Here are the parts relevant to the shoutbox:
index.html
<div class="shoutbox">
<h1>Shout box <img src='./assets/img/refresh.png'/></h1>
<ul class="shoutbox-content"></ul>
<div class="shoutbox-form">
<h2>Write a message <span>×</span></h2>
<form action="./publish.php" method="post">
<label for="shoutbox-name">nickname </label> <input type="text" id="shoutbox-name" name="name"/>
<label class="shoutbox-comment-label" for="shoutbox-comment">message </label> <textarea id="shoutbox-comment" name="comment" maxlength='240'></textarea>
<input type="submit" value="Shout!"/>
</form>
</div>
</div>
With JavaScript we will insert the published shouts into the <ul> element. The form is hidden by default, and only revealed when the “Write a message” header is clicked.
Shoutbox with PHP and jQuery
The JavaScript Code
And here is our script.js
, which makes the above HTML work:
assets/js/script.js
$(function(){
// Storing some elements in variables for a cleaner code base
var refreshButton = $('h1 img'),
shoutboxForm = $('.shoutbox-form'),
form = shoutboxForm.find('form'),
closeForm = shoutboxForm.find('h2 span'),
nameElement = form.find('#shoutbox-name'),
commentElement = form.find('#shoutbox-comment'),
ul = $('ul.shoutbox-content');
// Replace :) with emoji icons:
emojione.ascii = true;
// Load the comments.
load();
// On form submit, if everything is filled in, publish the shout to the database
var canPostComment = true;
form.submit(function(e){
e.preventDefault();
if(!canPostComment) return;
var name = nameElement.val().trim();
var comment = commentElement.val().trim();
if(name.length && comment.length && comment.length < 240) {
publish(name, comment);
// Prevent new shouts from being published
canPostComment = false;
// Allow a new comment to be posted after 5 seconds
setTimeout(function(){
canPostComment = true;
}, 5000);
}
});
// Toggle the visibility of the form.
shoutboxForm.on('click', 'h2', function(e){
if(form.is(':visible')) {
formClose();
}
else {
formOpen();
}
});
// Clicking on the REPLY button writes the name of the person you want to reply to into the textbox.
ul.on('click', '.shoutbox-comment-reply', function(e){
var replyName = $(this).data('name');
formOpen();
commentElement.val('@'+replyName+' ').focus();
});
// Clicking the refresh button will force the load function
var canReload = true;
refreshButton.click(function(){
if(!canReload) return false;
load();
canReload = false;
// Allow additional reloads after 2 seconds
setTimeout(function(){
canReload = true;
}, 2000);
});
// Automatically refresh the shouts every 20 seconds
setInterval(load,20000);
function formOpen(){
if(form.is(':visible')) return;
form.slideDown();
closeForm.fadeIn();
}
function formClose(){
if(!form.is(':visible')) return;
form.slideUp();
closeForm.fadeOut();
}
// Store the shout in the database
function publish(name,comment){
$.post('publish.php', {name: name, comment: comment}, function(){
nameElement.val("");
commentElement.val("");
load();
});
}
// Fetch the latest shouts
function load(){
$.getJSON('./load.php', function(data) {
appendComments(data);
});
}
// Render an array of shouts as HTML
function appendComments(data) {
ul.empty();
data.forEach(function(d){
ul.append('<li>'+
'<span class="shoutbox-username">' + d.name + '</span>'+
'<p class="shoutbox-comment">' + emojione.toImage(d.text) + '</p>'+
'<div class="shoutbox-comment-details"><span class="shoutbox-comment-reply" data-name="' + d.name + '">REPLY</span>'+
'<span class="shoutbox-comment-ago">' + d.timeAgo + '</span></div>'+
'</li>');
});
}
});
The Emoji One library has version for both JavaScript and PHP. In the appendComments method, we use the emojione.toImage() function to convert all typed-out smileys into emoji. See all functions that are supported, and check out this handy emoji code website. Now that the frontend is ready, let’s move on to the backend.
The PHP Code
We have two files – publish.php and load.php. The first accepts a POST request for storing shouts in the data store, and the second returns the 20 latest shouts. These files are not opened directly by visitors – they only handle AJAX requests.
publish.php
<?php
// Include our composer libraries
require 'vendor/autoload.php';
// Configure the data store
$dir = __DIR__.'/data';
$config = new JamesMossFlywheelConfig($dir, array(
'formatter' => new JamesMossFlywheelFormatterJSON,
));
$repo = new JamesMossFlywheelRepository('shouts', $config);
// Store the posted shout data to the data store
if(isset($_POST["name"]) && isset($_POST["comment"])) {
$name = htmlspecialchars($_POST["name"]);
$name = str_replace(array("n", "r"), '', $name);
$comment = htmlspecialchars($_POST["comment"]);
$comment = str_replace(array("n", "r"), '', $comment);
// Storing a new shout
$shout = new JamesMossFlywheelDocument(array(
'text' => $comment,
'name' => $name,
'createdAt' => time()
));
$repo->store($shout);
}
Here we directly use the Flywheel library we mentioned in the beginning. Once it is configured, you can store any type of data, which will be written as a JSON file in the data/shouts folder. Reading these shouts is done in load.php:
load.php
<?php
require 'vendor/autoload.php';
// If you want to delete old comments, make this true. We use it to clean up the demo.
$deleteOldComments = false;
// Setting up the data store
$dir = __DIR__.'/data';
$config = new JamesMossFlywheelConfig($dir, array(
'formatter' => new JamesMossFlywheelFormatterJSON,
));
$repo = new JamesMossFlywheelRepository('shouts', $config);
// Delete comments which are more than 1 hour old if the variable is set to be true.
if($deleteOldComments) {
$oldShouts = $repo->query()
->where('createdAt', '<', strtotime('-1 hour'))
->execute();
foreach($oldShouts as $old) {
$repo->delete($old->id);
}
}
// Send the 20 latest shouts as json
$shouts = $repo->query()
->orderBy('createdAt ASC')
->limit(20,0)
->execute();
$results = array();
$config = array(
'language' => 'RelativeTimeLanguagesEnglish',
'separator' => ', ',
'suffix' => true,
'truncate' => 1,
);
$relativeTime = new RelativeTimeRelativeTime($config);
foreach($shouts as $shout) {
$shout->timeAgo = $relativeTime->timeAgo($shout->createdAt);
$results[] = $shout;
}
header('Content-type: application/json');
echo json_encode($results);
We’ve included code which deletes shouts older than an hour. We use this feature to keep the demo clean. You can enable it if you wish. After selecting the shouts, we are also calculating the human-readable relative time stamp with the RelativeTime library.
With this our shoutbox is ready! You can embed it in your website, customize it and change the code any way you please. We hope you like it!
These days you can do pretty much anything with JavaScript and HTML. Thanks to Node-WebKit, we can even create desktop applications that feel native, and have full access to every part of the operating system. In this short tutorial, we will show you how to create a simple desktop application using Node-WebKit, which combines jQuery and a few Node.js modules.
Node-WebKit is a combination of Node.js and an embedded WebKit browser. The JavaScript code that you write is executed in a special environment and has access to both standard browser APIs and Node.js. Sounds interesting? Keep reading!
Update (15th Jan 2015) – Just after a week of publishing this tutorial, node-webkit was renamed to NW.js. For now, this tutorial is perfectly compatible with it. In the future we might update it if there are any changes.
Installing Node-WebKit
For developing applications, you will need to download the node-webkit executable, and call it from your terminal when you want to run your code. (Later you can package everything in a single program so your users can only click an icon to start it).
Head over to the project page and download the executable that is built for your operating system. Extract the archive somewhere on your computer. To start it, you need to do this in your terminal:
# If you are on linux/osx
/path/to/node-webkit/nw /your/project/folder
# If you are on windows
C:pathtonode-webkitnw.exe C:yourprojectfolder
# (the paths are only for illustrative purposes, any folder will do)
This will open a new node-webkit window and print a bunch of debug messages in your terminal.
You can optionally add the extracted node-webkit folder to your PATH, so that it is available as the nw
command from your terminal.
Your First Application
There is a Download button near the top of this article. Click it and get a zip with a sample app that we prepared for you. It fetches the most recent articles on Tutorialzine from our RSS feed and turns them into a cool looking 3D carousel using jQuery Flipster.
Directory Structure
Once you extract it, you will see the files above. From here this looks like a standard static website. However, it won’t work if you simply double click index.html – it requires Node.js modules, which is invalid in a web browser. To run it, CD into this folder, and try running the app with this command:
/path/to/node-webkit/nw .
This will show our glorious desktop app.
Our node-webkit app
How it was made
It all starts with the package.json file, which node-webkit looks up when starting. It describes what node-webkit should load and various parameters of the window.
package.json
{
"name": "nw-app",
"version": "1.0.0",
"description": "",
"main": "index.html",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"window": {
"toolbar": false,
"width": 800,
"height": 500
},
"license": "ISC",
"dependencies": {
"pretty-bytes": "^1.0.2"
}
}
The window
property in this file tells node-webkit to open a new window 800 by 500px and hide the toolbar. The file pointed to by the main
property will be loaded. In our case this is index.html:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tutorialzine Node-Webkit Experiment</title>
<link rel="stylesheet" href="./css/jquery.flipster.min.css">
<link rel="stylesheet" href="./css/styles.css">
</head>
<body>
<div class="flipster">
<ul>
<!-- Tutorialzine's latest articles will show here -->
</ul>
</div>
<p class="stats"></p>
<script src="./js/jquery.min.js"></script>
<script src="./js/jquery.flipster.min.js"></script>
<script src="./js/script.js"></script>
</body>
</html>
And finally, here is our JavaScript file. This is where it gets interesting!
js/script.js
// Mixing jQuery and Node.js code in the same file? Yes please!
$(function(){
// Display some statistic about this computer, using node's os module.
var os = require('os');
var prettyBytes = require('pretty-bytes');
$('.stats').append('Number of cpu cores: <span>' + os.cpus().length + '</span>');
$('.stats').append('Free memory: <span>' + prettyBytes(os.freemem())+ '</span>');
// Node webkit's native UI library. We will need it for later
var gui = require('nw.gui');
// Fetch the recent posts on Tutorialzine
var ul = $('.flipster ul');
// The same-origin security policy doesn't apply to node-webkit, so we can
// send ajax request to other sites. Let's fetch Tutorialzine's rss feed:
$.get('http://feeds.feedburner.com/Tutorialzine', function(response){
var rss = $(response);
// Find all articles in the RSS feed:
rss.find('item').each(function(){
var item = $(this);
var content = item.find('encoded').html().split('</a></div>')[0]+'</a></div>';
var urlRegex = /(http|ftp|https)://[w-_]+(.[w-_]+)+([w-.,@?^=%&:/~+#]*[w-@?^=%&/~+#])?/g;
// Fetch the first image of the article
var imageSource = content.match(urlRegex)[1];
// Create a li item for every article, and append it to the unordered list
var li = $('<li><img /><a target="_blank"></a></li>');
li.find('a')
.attr('href', item.find('link').text())
.text(item.find("title").text());
li.find('img').attr('src', imageSource);
li.appendTo(ul);
});
// Initialize the flipster plugin
$('.flipster').flipster({
style: 'carousel'
});
// When an article is clicked, open the page in the system default browser.
// Otherwise it would open it in the node-webkit window which is not what we want.
$('.flipster').on('click', 'a', function (e) {
e.preventDefault();
// Open URL with default browser.
gui.Shell.openExternal(e.target.href);
});
});
});
Notice that we are accessing Tutorialzine’s RSS feed directly with jQuery, even though it is on a different domain. This is not possible in a browser, but Node-WebKit removes this limitation to make development of desktop applications easier.
Here are the node modules we’ve used:
- Shell – A node webkit module that provides a collection of APIs that do desktop related jobs.
- OS – The built-in Node.js OS module, which has a method that returns the amount of free system memory in bytes.
- Pretty Bytes – Convert bytes to a human readable string: 1337 → 1.34 kB.
Our project also includes jQuery and the jQuery-flipster plugin, and that’s pretty much it!
Packaging and Distribution
You most certainly don’t want your users to go through the same steps in order to run you application. You wan’t to package it in a standalone program, and open it by simply double clicking it.
Packaging node-webkit apps for multiple operating systems takes a lot of work to do manually. But there are libraries that do this for you. We tried this npm module – https://github.com/mllrsohn/node-webkit-builder, and it worked pretty well.
The only disadvantage is that the executable files have a large size (they can easily hit 40-50mb) , because they pack a stripped down webkit browser and node.js together with your code and assets. This makes it rather impractical for small desktop apps (such as ours), but for larger apps it is worth a look.
Conclusion
Node-webkit is a powerful tool that opens a lot of doors to web developers. With it, you can easily create companion apps for your web services and build desktop clients which have full access to the users’s computer.
You can read more about node-webkit on their wiki.
When you collect information from people through a form, applying some kind of validation is a must. Failing to do so could lead to lost customers, junk data in your database or even security exploits of your website. Historically, building form validation has been a pain. On the server side, this is made easier by full stack frameworks which handle it for you, but on the client you often end up with JavaScript libraries that take a lot of effort to integrate.
Thankfully, HTML5 gives us a number of features that can handle most of your validation needs. Forms in HTML5 now have built-in support for validation through the use of special attributes and new input types, and give you a lot of control over styling with CSS.
See an online demo here and read a quick overview of the basics behind HTML5 form validation below.
1. Specialized Input Types
HTML5 introduced several new input types. They can be used to create input boxes, which will accept only a specified kind of data.
The new input types are as follows:
color
date
datetime
datetime-local
email
month
number
range
search
tel
time
url
week
To use one of the new types, include them as the value of the type attribute:
<input type="email"/>
If the browser does not support the given input type, the field will behave as a regular text input. Also, it’s helpful to know that some of the input fields (such as “email” and “tel”) will cause a mobile devices to open specialized keyboards with a limited set of keys, and not the full QUERTY layout.
For more details on all the input types, head out to this MDN wiki – here.
2. Required Fields
By simply adding the “required” attribute to a <input>, <select> or <textarea>, you tell the browser that a value must be provided in this field. Think of this as the red asterisk* we see in most registration forms.
<input type="checkbox" name="terms" required >
The problem here is that nearly any data will fulfill this requirement – for example you can bypass it with an empty space (we’ll show you how to prevent this in a moment).
When you set the required attribute on email or url fields, the browser expects a certain pattern to be followed, but it’s very permissive and emails such as “z@zz” are considered valid (read on to see how to work around this).
3. Limits
We can set some basic limitations like max length and minimum and maximum values for number fields. To limit the length of input fields and textareas, use the “maxlength” attribute. What this does is to forbid any string longer than the field’s “maxlength” value to be entered at all. If you try and paste a string witch exceeds this limit, the form will simply clip it.
<input type="text" name="name" required maxlength="15">
The <input type=”number”> fields use “max” and “min” attributes to create a range of possible values – in our example we’ve made the minimum allowed age to be 18 (too bad you can be whatever age you want on the internet).
<input type="number" name="age" min="18" required>
4. Styling
CSS3 pseudo classes let us style any form field depending on its state. They are:
:valid
:invalid
:required
:optional
:in-range
:out-of-range
:read-only
:read-write
This means that you can have required fields look one way, optional ones another and so on.
In our demo we’ve combined the “valid” and “invalid” selectors with the “focus” pseudo class to color form fields red or green when the user selects them and starts typing.
input:focus:invalid,
textarea:focus:invalid{
border:solid 2px #F5192F;
}
input:focus:valid,
textarea:focus:valid{
border:solid 2px #18E109;
background-color:#fff;
}
5. Tooltips
As you’ve probably noticed, when you try to submit a form that’s not correctly filled, a pop-up appears. By setting the “title” attribute of our fields, we can add additional hints on what values our rules validation expects.
Note that different browsers display the pop-ups in differently. In Chrome, the title attribute value will appear under the main error-message text in smaller font. In Firefox it doesn’t show your custom tooltip text at all, unless you’ve used the “pattern” attribute as well, which then is taken as a pattern info.
<input type="text" name="name" title="Please enter your user name.">
The error boxes or their default content can’t be changed that simply and require the help of JavaScript, but that’s a whole other tutorial.
6. Patterns
The “pattern” attribute lets developers set a Regular Expression, which the browser will match against the user supplied input, before allowing the form to be submitted. This gives us great control over the data entries, since RegEx patterns can be quite complex and precise. To learn more about Regular Expressions, check out our article – Learn Regular Expressions in 20 Minutes.
Now, with the ability to filter out input values, our example form accepts only full email addresses and a password with a minimum length of 8 characters, including at least one number.
Here’s how to use it:
<input type="email" name="email" required pattern="^S+@S+.S+$" title="example@mail.com">
We hope that this quick tip helped you get up to speed with HTML5’s validation features. Thanks for reading!
For a long time we have been forced to create the interfaces of our web apps using CSS layout modes which were originally designed for laying down documents. No wonder it is such a pain for beginners! Luckily, things are changing – we now have the flexbox layout mode. If you can afford to ignore IE < 10 (which is a big if), you can use it today! The spec is finalized, and most browsers already support it unprefixed.
In this quick tutorial, I will show you how to use flexbox to create something basic that has been surprisingly tricky to do until now – a sidebar that has equal height to your main content. The tricky part lies in that you want both your sidebar and main page element to have content with varying lengths, but at the same time they should have equal heights. With flex box, this is very easy. Here is how to do it.
Say hello to Flexbox
There are plenty of guides to give you an overview of flexbox. Here, we will take a more practical approach, and jump right into using it. First, here is the HTML:
<section id="page">
<div id="main">
<!-- The content of the page will go here -->
</div>
<aside>
<!-- This is the sidebar -->
</aside>
</section>
We want to make the #main div and the aside show next to each other, and be of equal heights, regardless of the content that they hold. We also want to make the page responsive, so that the sidebar has a fixed width, but the main element shrinks/expands to fill the available space. Lastly, on small screens, we want the sidebar to go beneath the main content.
Equal Height Sidebar with Flexbox
Here is how to do it. First, we will activate flexbox:
#page {
display:flex;
}
This will turn the #page element into a flex container. It will be displayed as a block level element (it will take the full width of the page), and will turn all the elements inside it into flex items. This is the important bit – we want our main content and sidebar to be flex items, so they take the full height of the page.
But we also want to make the #page element 1200px wide as a maximum, and we want it to be centered. It is still a normal block element, so we can center it as such:
#page {
display:flex;
/* Centering the page */
max-width:1200px;
margin:0 auto;
}
Perfect! Now we have to give widths to the #main element and the sidebar:
#main {
/* This makes the element grow and take all available space, not taken by the sidebar */
flex-grow:1;
}
aside {
/* Give the sidebar a default width, and prevent it from shrinking */
flex-shrink:0;
width:280px;
}
Done! Our layout is nearly finished. When the content of the #main element causes it to become longer, it will enlarge the #page, which in turn would also grow the aside (and the other way around). The last thing to do is make the sidebar go beneath the main content on small screens, which can be done with a simple media query:
@media all and (max-width: 800px) {
#page {
flex-flow:column;
}
/* Make the sidebar take the entire width of the screen */
aside {
width:auto;
}
}
By default flex containers have a flex-flow
value of row, which shows elements side-by-side. On small screens we are switching it to vertical orientation, which pushes the sidebar below the main content.
I’ve omitted some of the CSS that does not pertain to the layout for brevity. You can download the entire example from the button near the top of this article. It is worth mentioning that I have not provided fallbacks for IE 10, which implements a slightly older version of the flexbox spec, so it only works in Firefox, Chrome, Safari, Opera and IE 11.
With this our sidebar is ready!
Conclusion
There is more to learn about flexbox, but I hope that this article gives you a quick start. Flexbox is by no means the only way to create this layout, but it is the way forward for building web interfaces. If you can afford to drop support for older IE, you can use it today.
Listening to what your visitors have to say, is always beneficial when planning new features or changes in your website. For a long time we’ve been limited to just setting up a contact form and hoping that quality feedback will follow, which unfortunately is not always the case.
Today we are taking things up a notch – we are applying the same social principles that have brought success to sharing sites such as Digg and delicious, and encourage visitors to suggest and vote on features that they want implemented on your website.
The XHTML
Starting with the new HTML5 doctype, we define the opening and closing head and title tags, and include the main stylesheet of the app – styles.css, in the document.
suggestions.php
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Feature Suggest w/ PHP, jQuery & MySQL | Tutorialzine Demo</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<div id="page">
<div id="heading" class="rounded">
<h1>Feature Suggest<i>for Tutorialzine.com</i></h1>
</div>
<!-- The generated suggestion list comes here -->
<form id="suggest" action="" method="post">
<p>
<input type="text" id="suggestionText" class="rounded" />
<input type="submit" value="Submit" id="submitSuggestion" />
</p>
</form>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="script.js"></script>
</body>
</html>
After this comes the body tag and the #page div, which is the main container element. It holds the heading, the unordered list with all the suggestions (which is generated by PHP, as you will see in a moment), and the submit form.
Lastly we include the jQuery library from Google’s AJAX Library CDN, and our own script.js file, which is discussed in detail in the last section of this tutorial.
Feature Suggest App w/ jQuery, PHP, MySQL
The Table Schema
The app uses two MySQL tables to store data. Suggestions and Suggestions_votes. The first table contains the text of the suggestion and data such as rating and the number of votes the item has received. The second table keeps record of the IPs of the voters and prevents more than one vote to be cast in a single day per IP.
Suggestion Table Schema
To speed up the selection queries, an index is defined on the rating field. This helps when showing the suggestions ordered by popularity.
The suggestion votes table has a primary key consisting of three fields – the suggestion_id, the IP of the voter, and the date of the vote. And because primary keys do not allow for duplicate rows, we can be sure that users can vote only once per day by just checking the value of the affected_rows variable after the insert.
Suggestions_Votes Schema
The PHP
Before delving into the generation of the suggestion items and the AJAX interactions, first we have to take a look at the suggestion PHP class. It uses two PHP magic methods (apart from the constructor) to provide rich functionality to our code. When generating the front page, PHP runs a MySQL select query against the database, and creates an object of this class for every table row. The columns of the row are added as properties to the object.
suggestion.class.php
class Suggestion
{
private $data = array();
public function __construct($arr = array())
{
if(!empty($arr)){
// The $arr array is passed only when we manually
// create an object of this class in ajax.php
$this->data = $arr;
}
}
public function __get($property){
// This is a magic method that is called if we
// access a property that does not exist.
if(array_key_exists($property,$this->data)){
return $this->data[$property];
}
return NULL;
}
public function __toString()
{
// This is a magic method which is called when
// converting the object to string:
return '
<li id="s'.$this->id.'">
<div class="vote '.($this->have_voted ? 'inactive' : 'active').'">
<span class="up"></span>
<span class="down"></span>
</div>
<div class="text">'.$this->suggestion.'</div>
<div class="rating">'.(int)$this->rating.'</div>
</li>';
}
}
The __toString() method is used to create a string representation of the object. With its help we can build the HTML markup, complete with the suggestion title and number of votes.
The __get() method is used to route the access to undefined properties of the class to the $data array. This means that if we access $obj->suggestion, and this property is undefined, it is going to be fetched from the $data array, and returned to us as if it existed. This way we can just pass an array to the constructor, instead of setting up all the properties. We are using this when creating an object in ajax.php.
Now lets proceed with the generation of the unordered list on the front page.
suggestions.php
require "connect.php";
require "suggestion.class.php";
// Converting the IP to a number. This is a more effective way
// to store it in the database:
$ip = sprintf('%u',ip2long($_SERVER['REMOTE_ADDR']));
// The following query uses a left join to select
// all the suggestions and in the same time determine
// whether the user has voted on them.
$result = $mysqli->query("
SELECT s.*, if (v.ip IS NULL,0,1) AS have_voted
FROM suggestions AS s
LEFT JOIN suggestions_votes AS v
ON(
s.id = v.suggestion_id
AND v.day = CURRENT_DATE
AND v.ip = $ip
)
ORDER BY s.rating DESC, s.id DESC
");
$str = '';
if(!$mysqli->error)
{
// Generating the UL
$str = '<ul class="suggestions">';
// Using MySQLi's fetch_object method to create a new
// object and populate it with the columns of the result query:
while($suggestion = $result->fetch_object('Suggestion')){
$str.= $suggestion; // Uses the __toString() magic method.
}
$str .='</ul>';
}
After running the query, we use the fetch_object() method of the $result object. This method creates an object of the given class for every row in the result, and assigns the columns of that row to the object as public properties.
PHP also manages the AJAX requests sent by jQuery. This is done in ajax.php. To distinguish one AJAX action from another, the script takes a $_GET['action'] parameter, which can have one of two values – ‘vote‘ or ‘submit‘.
ajax.php
require "connect.php";
require "suggestion.class.php";
// If the request did not come from AJAX, exit:
if($_SERVER['HTTP_X_REQUESTED_WITH'] !='XMLHttpRequest'){
exit;
}
// Converting the IP to a number. This is a more effective way
// to store it in the database:
$ip = sprintf('%u',ip2long($_SERVER['REMOTE_ADDR']));
if($_GET['action'] == 'vote'){
$v = (int)$_GET['vote'];
$id = (int)$_GET['id'];
if($v != -1 && $v != 1){
exit;
}
// Checking to see whether such a suggest item id exists:
if(!$mysqli->query("SELECT 1 FROM suggestions WHERE id = $id")->num_rows){
exit;
}
// The id, ip and day fields are set as a primary key.
// The query will fail if we try to insert a duplicate key,
// which means that a visitor can vote only once per day.
$mysqli->query("
INSERT INTO suggestions_votes (suggestion_id,ip,day,vote)
VALUES (
$id,
$ip,
CURRENT_DATE,
$v
)
");
if($mysqli->affected_rows == 1)
{
$mysqli->query("
UPDATE suggestions SET
".($v == 1 ? 'votes_up = votes_up + 1' : 'votes_down = votes_down + 1').",
rating = rating + $v
WHERE id = $id
");
}
}
else if($_GET['action'] == 'submit'){
// Stripping the content
$_GET['content'] = htmlspecialchars(strip_tags($_GET['content']));
if(mb_strlen($_GET['content'],'utf-8')<3){
exit;
}
$mysqli->query("INSERT INTO suggestions SET suggestion = '".$mysqli->real_escape_string($_GET['content'])."'");
// Outputting the HTML of the newly created suggestion in a JSON format.
// We are using (string) to trigger the magic __toString() method.
echo json_encode(array(
'html' => (string)(new Suggestion(array(
'id' => $mysqli->insert_id,
'suggestion' => $_GET['content']
)))
));
}
When jQuery fires the ‘vote‘ request, it does not expect any return values, so the script does not output any. In the ‘submit‘ action, however, jQuery expects a JSON object to be returned, containing the HTML markup of the suggestion that was just inserted. This is where we create a new Suggestion object for the sole purpose of using its __toString() magic method and converting it with the inbuilt json_encode() function.
Suggest & Vote on Features
The jQuery
All of the jQuery code resides in script.js. It listens for click events on the green and red arrows. But as suggestions can be inserted at any point, we are using the live() jQuery method, so we can listen for the event even on elements that are not yet created.
script.js
$(document).ready(function(){
var ul = $('ul.suggestions');
// Listening of a click on a UP or DOWN arrow:
$('div.vote span').live('click',function(){
var elem = $(this),
parent = elem.parent(),
li = elem.closest('li'),
ratingDiv = li.find('.rating'),
id = li.attr('id').replace('s',''),
v = 1;
// If the user's already voted:
if(parent.hasClass('inactive')){
return false;
}
parent.removeClass('active').addClass('inactive');
if(elem.hasClass('down')){
v = -1;
}
// Incrementing the counter on the right:
ratingDiv.text(v + +ratingDiv.text());
// Turning all the LI elements into an array
// and sorting it on the number of votes:
var arr = $.makeArray(ul.find('li')).sort(function(l,r){
return +$('.rating',r).text() - +$('.rating',l).text();
});
// Adding the sorted LIs to the UL
ul.html(arr);
// Sending an AJAX request
$.get('ajax.php',{action:'vote',vote:v,'id':id});
});
$('#suggest').submit(function(){
var form = $(this),
textField = $('#suggestionText');
// Preventing double submits:
if(form.hasClass('working') || textField.val().length<3){
return false;
}
form.addClass('working');
$.getJSON('ajax.php',{action:'submit',content:textField.val()},function(msg){
textField.val('');
form.removeClass('working');
if(msg.html){
// Appending the markup of the newly created LI to the page:
$(msg.html).hide().appendTo(ul).slideDown();
}
});
return false;
});
});
When a click on one of those arrows occurs, jQuery determines whether the ‘inactive’ class is present on the LI element. This class is only assigned to the suggestion, if the user has voted during the last day, and, if present, the script will ignore any click events.
Notice how $.makeArray is used to turn the jQuery objects, containing the LI elements, into a true array. This is done, so we can use the array.sort() method and pass it a custom sort function, which takes two LIs at the same time and outputs a negative integer, zero or a positive integer depending on which of the two elements has a grater rating. This array is later inserted into the unordered list.
The CSS
Now that we have all the markup generated, we can move on with the styling. As the styling is pretty much trivial, I only want to show you the class that rounds the top-left and bottom-right corners of the elements that it is applied to. You can see the rest of the CSS rules in styles.css.
styles.css
.rounded,
#suggest,
.suggestions li{
-moz-border-radius-topleft:12px;
-moz-border-radius-bottomright:12px;
-webkit-border-top-left-radius:12px;
-webkit-border-bottom-right-radius:12px;
border-top-left-radius:12px;
border-bottom-right-radius:12px;
}
Notice that the Mozilla syntax differs from the standard in the way it targets the different corners of the element. Keeping that in mind, we can apply this class to pretty much every element, as you can see from the demonstration.
With this our Feature Suggest App is complete!
Conclusion
If you plan to set up this script on your own server, you would need to create the two suggestion tables by running the code found in tables.sql in the SQL tab of phpMyAdmin. Also remember to fill in your database connection details in connect.php.
You can use this script to gather precious feedback from your visitors. You can also disable the option for users to add new suggestions, and use it as a kind of an advanced poll system.
Be sure to share your thoughts in your comment section below.
In this tutorial, we are making a dynamic FAQ section. The script, with the help of jQuery & YQL, will pull the contents of a shared spreadsheet in your Google Docs account, and use the data to populate the FAQ section with questions and answers.
The best aspect of this solution, is that you can change the contents of the FAQ section from within Google Docs – just edit the spreadsheet. You can even leverage the rest of Google Docs’ features, such as collaborative editing. This way, a small team can support the FAQ section without the need of a dedicated CMS solution.
Thanks to Chris Ivarson for designing the Google Docs icon, used in the featured illustration of this tutorial.
Google Docs
Before starting work on the FAQ section, we first need to create a new Google Docs Spreadsheet. As you probably already have an account with Google (if not, create one), we’ll move straight to the interesting part.
In a blank spreadsheet, start filling in two columns of data. The first column should contain the questions, and the second one the answers, that are going to become entries in your FAQ section. Each FAQ goes on a separate line. You can see the one that I created here.
After this, click Share > Publish as webpage and choose CSV from the dropdown list. This will generate a link to your spreadsheet in the form of a regular CSV file, which we will use later.
The HTML
The first step in developing the script is the markup. The #page div is the main container element. It is the only div with an explicit width. It is also centered in the middle of the page with a margin:auto, as you will see in the CSS part of the tut.
faq.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<div id="page">
<div id="headingSection">
<h1>Frequently Asked Questions</h1>
<a class="button expand" href="#">Expand</a>
</div>
<div id="faqSection">
<!-- The FAQs are inserted here -->
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="script.js"></script>
</body>
</html>
The stylesheet is included in the head of the document, and the jQuery library and our script.js are included at the bottom. All of these are discussed in detail in the next sections of this tutorial.
The #headingSection contains the h1 heading, and the expand/collapse button. After it comes the #faqSection div, where the FAQ entries are inserted after jQuery fetched the contents of your Google Docs Spreadsheet.
The FAQ entries are organized as a definition list structure (dl). This is one of the least used HTML elements, but is perfect for our task. Here is how it looks once jQuery adds it to the page.
faq.html
<dl>
<dt><span class="icon"></span>How does this FAQ section work?</dt>
<dd>With the help of jQuery and YQL, this script pulls the latest data ..</dd>
<dt><span class="icon"></span>Can you modify it?</dt>
<dd>This is the best part of it - you can change the contents ..</dd>
</dl>
The dl element holds a dt for each question and a dd for each answer. The dd elements are hidden with display:none, and are only shown with a slideDown animation once the respective dt is clicked.
Dynamic FAQ Section
The CSS
The styles, (held in styles.css) are pretty straightforward and self-explanatory. As mentioned above, only the #page div, which acts as the main container, is explicitly assigned a width. It is also centered in the middle of the page with an auto value for the left/right margins.
styles.css – Part 1
#page{
width:753px;
margin:50px auto;
}
#headingSection{
background-color:#7b8b98;
padding:40px;
padding-left:60px;
position:relative;
border:1px solid #8b9ba7;
border-bottom:none;
}
#faqSection{
background:url('img/faq_bg.jpg') repeat-y #fff;
padding:20px 90px 60px 60px;
border:1px solid white;
text-shadow:1px 1px 0 white;
}
h1{
color:#fff;
font-size:36px;
font-weight:normal;
}
/* The expand / collapse button */
a.button{
background:url('img/buttons.png') no-repeat;
width:80px;
height:38px;
position:absolute;
right:50px;
top:45px;
text-indent:-9999px;
overflow:hidden;
border:none !important;
}
a.button.expand:hover{ background-position:0 -38px;}
a.button.collapse{ background-position:0 -76px;}
a.button.collapse:hover{ background-position:0 bottom;}
We are using a single anchor tag for both the expand and the collapse button, by assigning it either the expand or the collapse CSS class. These classes determine which parts of the background image are offset into view. The background image itself is four times the height of the button, and contains a normal and a hover state for both the expand and collapse button versions.
styles.css – Part 2
/* Definition Lists */
dt{
color:#8F9AA3;
font-size:25px;
margin-top:30px;
padding-left:25px;
position:relative;
cursor:pointer;
border:1px solid transparent;
}
dt:hover{ color:#5f6a73;}
dt .icon{
background:url('img/bullets.png') no-repeat;
height:12px;
left:0;
position:absolute;
top:11px;
width:12px;
}
dt.opened .icon{ background-position:left bottom;}
dd{
font-size:14px;
color:#717f89;
line-height:1.5;
padding:20px 0 0 25px;
width:580px;
display:none;
}
When a definition title (dt) is clicked, its respective dd is expanded into view (as you will see in the next section). With this, the dt is also assigned an opened class. This class helps jQuery determine which FAQ’s are opened, and in the same time affects the styling of the small bullet on the left of the title.
FAQ expanded
The jQuery
Moving to probably the most interesting part of the tutorial. If you’ve been following the tutorials on this site, you’ve probably noticed that YQL finds its way into a lot of the tutorials here. The main reason behind this, is that YQL makes possible for developers to use it as a proxy for a wide range of APIs, and implement a diverse functionality entirely in JavaScript.
Today we are using YQL to fetch our Google Spreadsheet as CSV and parse it, so that it is available as a regular JSON object. This way, we end up with a free and easy to update data storage for our simple app.
script.js
$(document).ready(function(){
// The published URL of your Google Docs spreadsheet as CSV:
var csvURL = 'https://spreadsheets.google.com/pub?key='+
'0Ahe1-YRnPKQ_dEI0STVPX05NVTJuNENhVlhKZklNUlE&hl=en&output=csv';
// The YQL address:
var yqlURL = "http://query.yahooapis.com/v1/public/yql?q="+
"select%20*%20from%20csv%20where%20url%3D'"+encodeURIComponent(csvURL)+
"'%20and%20columns%3D'question%2Canswer'&format=json&callback=?";
$.getJSON(yqlURL,function(msg){
var dl = $('<dl>');
// Looping through all the entries in the CSV file:
$.each(msg.query.results.row,function(){
// Sometimes the entries are surrounded by double quotes. This is why
// we strip them first with the replace method:
var answer = this.answer.replace(/""/g,'"').replace(/^"|"$/g,'');
var question = this.question.replace(/""/g,'"').replace(/^"|"$/g,'');
// Formatting the FAQ as a definition list: dt for the question
// and a dd for the answer.
dl.append('<dt><span class="icon"></span>'+
question+'</dt><dd>'+answer+'</dd>');
});
// Appending the definition list:
$('#faqSection').append(dl);
$('dt').live('click',function(){
var dd = $(this).next();
// If the title is clicked and the dd is not currently animated,
// start an animation with the slideToggle() method.
if(!dd.is(':animated')){
dd.slideToggle();
$(this).toggleClass('opened');
}
});
$('a.button').click(function(){
// To expand/collapse all of the FAQs simultaneously,
// just trigger the click event on the DTs
if($(this).hasClass('collapse')){
$('dt.opened').click();
}
else $('dt:not(.opened)').click();
$(this).toggleClass('expand collapse');
return false;
});
});
});
It may not be clear from the code above, but jQuery sends a JSONP request to YQL’s servers with the following YQL query:
SELECT * FROM csv
WHERE url='https://spreadsheets.google.com/...'
AND columns='question,answer'
CSV is a YQL table, which takes the URL of a csv file and a list of column names. It returns a JSON object with the column names as its properties. The script then filters them (stripping off unnecessary double quotes) and inserts them as a definition list (DL) into the page.
With this our dynamic FAQ section is complete!
Customization
To use this script with your own spreadsheet, you only need to edit the csvURL variable in script.js, and replace it with the CSV URL of your spreadsheet. You can obtain this address from Share > Publish as webpage > CSV dropdown. Also, be aware that when you make changes to the spreadsheet, it could take a few minutes for the changes to take effect. You can speed this up by clicking the Republish now button, found in the same overlay window.
Obtaining the CSV URL
Final Words
You can use the same technique to power different kinds of dynamic pages. However, this implementation does have its shortcomings. All the content is generated with JavaScript, which means that it will not be visible to search engines.
To guarantee that your data will be crawled, you can take a different route. You could use PHP, or other back-end language, to fetch and display the data from YQL in a fixed interval of time – say 30 minutes (or even less frequently, if you don’t plan to update the data often).
Be sure to share your suggestions in the comment section below.
In this tutorial, we are using jQuery UI’s autocomplete widget, to build a simple AJAX movie search form. The script is going to use TheMovieDatabase.org‘s free API, to provide auto suggestions against a vast database of movie titles.
For those of you who might not be familiar with TMDb.org, this is an open, community driven movie database. It is similar to IMDb, which you’ve probably heard about, but also provides a number of useful API’s for developers.
Prerequisites
Before being able to use the API, you need to get a free developer key from TMDb after a quick registration. After this, remember to copy your key to movieInfo.php from the download archive.
Step 1 – XHTML
The markup consists of the two main div containers – #logo and #holder. The former holds the icon and the logo text in the form of transparent PNG images (defined as backgrounds to the respective divs), while the latter contains the search form and the submit button.
movieApp.html
<div id="page">
<div id="logo">
<div id="icon"></div>
<div id="movieAppLabel"></div>
</div>
<div id="holder">
<form action="http://www.themoviedb.org/search" method="post" target="_blank">
<fieldset>
<input type="text" id="movieName" name="search" />
</fieldset>
</form>
<a href="#" class="button">Submit</a>
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js"></script>
<script src="script.js"></script>
Notice that the action attribute of the form is pointed at TMDB’s search page. The search terms are passed via POST with the #movieName text field. You can test it by filling in a movie name and submitting the form.
Lastly in the page are included jQuery, jQuery UI and our own script file. We are using jQuery UI’s autocomplete widget to display a drop down list of movie titles which are fetched from TMDb’s API. You can see the markup that is generated by the widget below.
<input class="ui-autocomplete-input"/>
<ul class="ui-autocomplete ui-menu ui-widget ui-widget-content ui-corner-all">
<li class="ui-menu-item">
<a class="ui-corner-all">item 1</a>
</li>
<li class="ui-menu-item">
<a class="ui-corner-all">item 2</a>
</li>
<li class="ui-menu-item">
<a class="ui-corner-all">item 3</a>
</li>
</ul>
This code is generated automatically by the widget and appended before the closing body tag.
Simple Movie Search App
Step 2 – PHP
When you start typing a movie title in the text box of the form, an AJAX request is sent to moveInfo.php. This script sends a search request to TMDb’s API, with our developer key. The service returns a JSON object with suitable movie titles. The script processes them and outputs them back as a response to the AJAX request.
Lets take a closer look at how this works.
movieInfo.php
/**
* Define your API key below. To obtain one, visit
* http://www.themoviedb.org/account/signup
*/
$api_key = '...';
// If the request was not issued by AJAX, or
// the search term is missing, exit:
if(!$_SERVER["HTTP_X_REQUESTED_WITH"] || !$_GET['term']){
exit;
}
include 'tmdbAPI/TMDb.php';
$tmdb = new TMDb($api_key);
// Send a search API request to TMDb,
// and parse the returned JSON data:
$json = json_decode($tmdb->searchMovie($_GET['term']));
$response = array();
$i=0;
foreach($json as $movie){
// Only movies existing in the IMDB catalog (and are not adult) are shown
if(!$movie->imdb_id || $movie->adult) continue;
if($i >= 8 ) break;
// The jQuery autocomplete widget shows the label in the drop down,
// and adds the value property to the text box.
$response[$i]['value'] = $movie->name;
$response[$i]['label'] = $movie->name . ' <small>(' . date('Y',strtotime($movie->released)).')</small>';
$i++;
}
// Presenting the response as a JSON object:
echo json_encode($response);
Luckily for us, there is a PHP class available, that handles all communication with the TMDb API. We just need to include it into the page, and provide the developer API key we received from TMDb. The search terms, that the user has entered into the search box, are available in $_GET['term']. Calling the searchMovie() method with these terms, will yield a JSON object, containing all kinds of information about the movies that match our search criteria. You can see a sample response below.
[{
"score": 8.750235,
"popularity": 3,
"translated": true,
"adult": false,
"language": "en",
"name": "The Hitchhiker's Guide to the Galaxy",
"alternative_name": "The Hitchhikers Guide to the Galaxy",
"movie_type": "movie",
"id": 7453,
"imdb_id": "tt0371724",
"url": "http://www.themoviedb.org/movie/7453",
"rating": 6.8,
"certification": "PG",
"overview": "Mere seconds before the Earth is to be demolished by an alien construction crew, Arthur Dent is swept off the planet by his friend Ford Prefect, a researcher penning a new edition of \"The Hitchhiker's Guide to the Galaxy.\"",
"released": "2005-04-20",
"posters": [{
"image": {
"type": "poster",
"size": "original",
"height": 1000,
"width": 675,
"url": "http://hwcdn.themoviedb.org/posters/16e/4bcc96cd017a3c0f2600016e/the-hitchhiker-s-guide-to-the-galaxy-original.jpg",
"id": "4bcc96cd017a3c0f2600016e"
}
}],
"version": 22,
"last_modified_at": "2010-07-19 22:59:02"
}]
The response contains the title of the movie, an overview, release date, a corresponding IMDB id, and even posters and fan art. We do not need most of this information, so PHP reduces it only to a title and a release year, after which outputs it in the form of a JSON object, ready to be used by the autocomplete. This brings us to the next step.
Step 3 – jQuery
As you know, jQuery comes with a lot of useful functionality in the form of plugins. There is also a dedicated extension of the library, for building user interfaces, known as jQuery UI. It gives developers widgets, which are ready for use and easy to customize. One of these widgets is the new autocomplete widget, introduced in the newer versions of the library.
Lets take a look at how it is used.
script.js
$(document).ready(function(){
// Caching the movieName textbox:
var movieName = $('#movieName');
// Defining a placeholder text:
movieName.defaultText('Type a Move Title');
// Using jQuery UI's autocomplete widget:
movieName.autocomplete({
minLength : 5,
source : 'movieInfo.php'
});
$('#holder .button').click(function(){
if(movieName.val().length && movieName.data('defaultText') != movieName.val()){
$('#holder form').submit();
}
});
});
// A custom jQuery method for placeholder text:
$.fn.defaultText = function(value){
var element = this.eq(0);
element.data('defaultText',value);
element.focus(function(){
if(element.val() == value){
element.val('').removeClass('defaultText');
}
}).blur(function(){
if(element.val() == '' || element.val() == value){
element.addClass('defaultText').val(value);
}
});
return element.blur();
}
To create an autocomplete, we just need to call the autocomplete() method. It takes a number of optional parameters. The most important ones are minLength (which prevents request to the server from being fired before a certain number of characters has been typed) and source, which determines the data that is shown in the drop down list.
Source can take either an array with strings, a URL (to which an AJAX request will be sent) or a callback function. In our case, the URL of movieInfo.php will suffice.
Here is a sample response, which is returned by movieInfo.php (this JSON object was compiled after a request to TMDb’s API for “Hitchhiker’s guide“).
[{
"value": "Hachiko: A Dog's Story",
"label": "Hachiko: A Dog's Story <small>(2009)<\/small>"
},
{
"value": "Teenage Hitch-hikers",
"label": "Teenage Hitch-hikers <small>(1975)<\/small>"
},
{
"value": "The Hitchhiker's Guide to the Galaxy",
"label": "The Hitchhiker's Guide to the Galaxy <small>(2005)<\/small>"
},
{
"value": "The Hitch-Hiker",
"label": "The Hitch-Hiker <small>(1953)<\/small>"
}]
Each object in the array contains a value and a label property. The label is only shown in the dropdown list, while the value is inserted into the textbox once the item is selected.
Custom Styled jQuery UI Autocomplete
Step 4 – CSS
Now that all the markup is generated and is in place, it is time to start beautifying.
styles.css – Part 1
#page{
width:600px;
margin:150px auto 0;
}
/* Logo */
#logo{
width:380px;
position:relative;
height:90px;
margin:0 auto;
}
#icon{
width:80px;
height:86px;
background:url('img/icon.png') no-repeat;
float:left;
}
#movieAppLabel{
width:268px;
height:58px;
background:url('img/logo_txt.png') no-repeat;
position:absolute;
right:0;
top:18px;
}
/* The Search Box & Holder */
#holder{
width:530px;
height:145px;
background:url('img/holder.png') no-repeat center center;
margin:30px auto;
position:relative;
}
#holder fieldset{
position:absolute;
top:52px;
left:40px;
border-bottom:1px solid #fff;
}
#holder input{
font-family:'Myriad Pro',Arial,Helvetica,sans-serif;
border:none;
border-bottom:1px solid #bbb;
background:none;
color:#8D8D8D;
font-size:20px;
padding:4px 0;
width:250px;
text-shadow:1px 1px #fff;
outline:none;
}
In the first part of the code, we style the #logo, and the #holder divs. The shutter icon and the logo text are defined as backgrounds to the #icon and #movieAppLabel divs respectively. Relative positioning is applied to the #holder so that it is easier to position the input box and the submit button.
styles.css – Part 2
fieldset{
border:none;
}
/* The Blue Button */
a.button{
background:url('img/buttons.png') no-repeat;
width:105px;
height:37px;
position:absolute;
top:52px;
right:42px;
text-indent:-9999px;
overflow:hidden;
border:none !important;
}
a.button:hover{
background-position:left bottom;
}
/* Styling the markup generated by the autocomplete jQuery UI widget */
ul.ui-autocomplete{
width:250px;
background-color:#f5f5f5;
border:1px solid #fff;
outline:1px solid #ccc;
}
ul.ui-autocomplete li{
list-style:none;
border-bottom:1px solid #e0e0e0;
border-top:1px solid #fff;
}
ul.ui-autocomplete li:first-child{
border-top:none;
}
ul.ui-autocomplete li:last-child{
border-bottom:none;
}
ul.ui-autocomplete li a{
color:#999;
border:none !important;
text-decoration:none !important;
padding:10px 17px;
display:block;
}
#ui-active-menuitem{
background-color:#fff;
color:#666;
cursor:pointer;
}
jQuery UI does come with its own styles, however they are rather clunky and do not fit well into the current design. This is why we are applying a number of rules (starting from line 23), which apply a custom design to the autocomplete widget. The structure of the widget is basically an unordered list, with each of the suggested items being a hyperlink in a li element. With this in mind (and after looking up the appropriate class names from the code in step one) we can safely style the drop down list and perfectly blend it with the rest of the design.
With this our Simple Movie Search App is complete!
To Wrap it Up
You can modify this script to use any kind of api and data. This can be a powerful tool, as it might assist users in typing search terms that they may not normally think of themselves. For example providing your product names as search suggestions, may be an effective tactic to expose more of your merchandise and improve sales.
What do you think? How would you improve this app?
When you publish something online, there are not that many ways to determine whether people like what you have to say. Comments, the cornerstone of blogging, are too demanding, and users often prefer not to post one. If you’ve dropped by Behance, you’ve probably noticed their appreciate badge, which is a neat solution to this exact problem. With it people share their appreciation for somebody’s work.
Today we are implementing such a badge, which you can include in every page of your website with a bit of jQuery magic. So go ahead and download the zip from the button above (PSD included!) and continue with the tutorial.
The Database Schema
The script we are doing today uses two tables. The first holds one record for each of the pages which have the appreciate button enabled. The second one stores the IP of the person that voted along the unique ID of the page. This way we can easily determine whether the person has previously voted for the page and display the appropriate version of the button (active or disabled).
Table Schema appreciate_pages
The hash field holds an MD5 sum of the URL of the page. This way we add an UNIQUE index which will speed up the selects we run on the records, as well ensure there are no duplicate records in the table. The appreciated column holds the number of appreciations of the pages.
Table Schema appreciate_votes
The appreciate_votes table contains the IP of the person that has voted (in the form of an integer), and the id of the page from the appreciate_pages table. The timestamp is automatically updated to the current time when an insert occurs.
You can create these two tables by running the code from tables.sql in the SQL section of phpMyAdmin from the downloadable archive, part of this tutorial.
Step 1 – XHTML
Lets start with the XHTML part of the tutorial. The markup of the page is extremely simple. To have the appreciate button functioning, you just need to provide a container in which the button is inserted, and an optional element, which holds the total number of clicks on the button. You can safely omit the latter one, leaving you with only one div to code.
page.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>An AJAX Click To Appreciate Badge</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<link rel="stylesheet" type="text/css" href="appreciateMe/appreciate.css"/>
</head>
<body>
<div id="countDiv"></div>
<div id="main"></div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script src="appreciateMe/plugin.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
</body>
</html>
In the page above, you can see that I am including two stylesheet files. The first is styles.css, which is used to style the page, and appreciate.css, which is located in the plugin directory, and is responsible for the styling of the appreciate button.
Before the closing body tag, you can see that I also include the jQuery library from Google’s CDN repository, the plugin.js file and script.js, which uses the plugin to create the button on the page. You will only need to change the contents of script.js to make the script working on your pages.
Click To Appreciate - looks good on both dark and light backgrounds
Step 2 – PHP
PHP handles the database interactions and is on the backend of the AJAX requests. Most of the script logic is located in c script.php which you can see below. But first lets take a look at connect.php, which handles the database connection.
appreciateMe/connect.php
$db_host = 'localhost';
$db_user = 'YourUsername';
$db_pass = 'YouPassword';
$db_name = 'NameOfDB';
@$mysqli = new mysqli($db_host, $db_user, $db_pass, $db_name);
if (mysqli_connect_errno()) {
die('<h1>Could not connect to the database</h1>');
}
$mysqli->set_charset("utf8");
Up until now, we’ve always used the old mysql extension for database connections under PHP, as it is a bit easier to use and I wanted to keep the code compatible with PHP 4. However, with the recent announcement that WordPress (our favorite blogging engine) will be dropping support for that version of PHP, I decided that it is time to also make the switch to the new version – MySQLi (MySQL improved).
As you can see from the code above, the only major difference with the old way we connected to a database, is that here we create a MySQLi object instead of using the mysql_ functions. Also, as you will see in a moment, when we query the database a MySQL resource object is returned, which in turn has its own set of methods. This might sound intimidating, but it will become perfectly clear once you see it in action.
appreciateMe/script.php
/* Setting the error reporting level */
error_reporting(E_ALL ^ E_NOTICE);
include 'connect.php';
if(!$_GET['url'] || !filter_input(INPUT_GET,'url',FILTER_VALIDATE_URL)){
exit;
}
$pageID = 0;
$appreciated = 0;
$jsonArray = array();
$hash = md5($_GET['url']);
$ip = sprintf('%u',ip2long($_SERVER['REMOTE_ADDR']));
// $result is an object:
$result = $mysqli->query("SELECT id,appreciated FROM appreciate_pages WHERE hash='".$hash."'");
if($result)
{
list($pageID,$appreciated) = $result->fetch_row();
// fetch_row() is a method of result
}
// The submit parameter denotes that we need to write to the database
if($_GET['submit'])
{
if(!$pageID)
{
// If the page has not been appreciated yet, insert a new
// record to the database.
$mysqli->query("
INSERT INTO appreciate_pages
SET
hash='".$hash."',
url='".$mysqli->real_escape_string($_GET['url'])."'"
);
if($mysqli->affected_rows){
// The insert_id property contains the value of
// the primary key. In our case this is also the pageID.
$pageID = $mysqli->insert_id;
}
}
// Write the vote to the DB, so the user can vote only once
$mysqli->query("
INSERT INTO appreciate_votes
SET
ip = ".$ip.",
pageid = ".$pageID
);
if($mysqli->affected_rows){
$mysqli->query("
UPDATE appreciate_pages
SET appreciated=appreciated+1 WHERE id=".$pageID
);
// Increment the appreciated field
}
$jsonArray = array('status'=>1);
}
else
{
// Only print the stats
$voted = 0;
// Has the user voted?
$res = $mysqli->query("
SELECT 1 FROM appreciate_votes
WHERE ip=".$ip." AND pageid=".$pageID
);
if($res->num_rows){
$voted = 1;
}
$jsonArray = array('status'=>1,'voted'=>$voted,'appreciated'=>$appreciated);
}
// Telling the browser to interpret the response as JSON:
header('Content-type: application/json');
echo json_encode($jsonArray);
The script handles two different types of AJAX requests – read only request (which returns a JSON object with information about the number of appreciations of the page, and whether the current user has clicked the button), and write requests (which save the visitor’s vote to the database, and if necessary, save the page URL and hash as well).
As you an see in the code snippet above, one of the first things that the script does is to calulate the MD5 hash of the page. This is used as a unique key in the database, as URLs have unlimited length which is incompatible with MySQL’s UNIQUE keys. As an MD5 hash is unique for most practical purposes, we can safely use it in our selects and inserts, instead of the long URL addresses.
In the last line of the code, we convert the $jsonArray array into a valid JSON object with the inbuilt json_encode PHP function, and output it with a applicatoin/json content type.
Click To Appreciate - Inactive
Step 3 – jQuery
Inside the appreciateMe directory you can find the plugin.js file. You must include it in the page you wish to show the Appreciate button on. It uses AJAX to request data from the PHP backend and uses the response it receives to create the markup of the button.
appreciateMe/plugin.js
function(){
$.appreciateButton = function(options){
// The options object must contain a URL and a Holder property
// These are the URL of the Appreciate php script, and the
// div in which the badge is inserted
if(!'url' in options || !'holder' in options){
return false;
}
var element = $(options.holder);
// Forming the url of the current page:
var currentURL = window.location.protocol+'//'+
window.location.host+window.location.pathname;
// Issuing a GET request. A rand parameter is passed
// to prevent the request from being cached in IE
$.get(options.url,{url:currentURL,rand:Math.random()},function(response){
// Creating the appreciate button:
var button = $('<a>',{
href:'',className:'appreciateBadge',
html:'Appreciate Me'
});
if(!response.voted){
// If the user has not voted previously,
// make the button active / clickable.
button.addClass('active');
}
else button.addClass('inactive');
button.click(function(){
if(button.hasClass('active')){
button.removeClass('active').addClass('inactive');
if(options.count){
// Incremented the total count
$(options.count).html(1 + parseInt(response.appreciated));
}
// Sending a GET request with a submit parameter.
// This will save the appreciation to the MySQL DB.
$.getJSON(options.url,{url:currentURL,submit:1});
}
return false;
});
element.append(button);
if(options.count){
$(options.count).html(response.appreciated);
}
},'json');
return element;
}
})(jQuery);
The script basically creates a new method in the main jQuery object. This differs from the plugins that we usually do, in that this type of plugins are not called on a set of elements (no need to select elements). You can just call $.appreciateButton() while passing a configuration object as a parameter. This is exactly what we’ve done in script.js add a button to the page:
script.js
$(document).ready(function(){
// Creating an appreciate button.
$.appreciateButton({
url : 'appreciateMe/script.php', // URL to the PHP script.
holder : '#main', // The button will be inserted here.
count : '#countDiv' // Optional. Will show the total count.
});
});
The configuration object, which is passed as a parameter, has to contain a url and a holder properties, whereas count is optional. Notice that I’ve specified the path to script.php relatively, as appreciateMe is a child directory of the one the page is currently in.
However, if you plan to add the script to a site with a variable path structure, you should probably specify an absolute path. Either add a leading slash, or provide a complete URL with http://.
Step 4 – CSS
Now that we have all the markup and code in place, it is time to turn to the styling. The CSS rules that style the appreciate badge are located in appreciate.css. You could optionally copy these rules to your main stylesheet file, if you’d like to avoid the extra request, but beware that you will need to change the paths to the background images.
appreciateMe/appreciate.css
.appreciateBadge{
width:129px;
height:129px;
display:block;
text-indent:-9999px;
overflow:hidden;
background:url('sprite.png') no-repeat;
text-decoration:none;
border:none;
}
.appreciateBadge.active{
background-position:left top;
}
.appreciateBadge.active:hover{
background-position:0 -129px;
}
.appreciateBadge.inactive{
background-position:left bottom;
cursor:default;
}
There are three versions of the appreciate badge image. A default one, a hover one, and an inactive one. All three of these reside in the same file – sprite.png, one below the other. With this technique you can switch between the versions instantaneously by offsetting the background image of the hyperlink.
styles.css
#main{
margin:80px auto;
width:130px;
}
#countDiv{
color:#eee;
font-size:35px;
margin-right:120px;
position:absolute;
right:50%;
top:265px;
}
You can find the rest of the styles, which refine the looks of page.html, in styles.css. Only two sets of styles affect the appreciate button directly. The #main div, which contains the button and centers it on the page, and #countDiv in which the total number of appreciations is inserted.
With this our Click to Appreciate Badge is complete!
Conclusion
Before being able to run this script on your server, you first have to replace the MySQL credentials in connect.php with your own. Also, you will need to run the contents of tables.sql in the SQL tab of phpMyAdmin, so the two tables are created. Lastly, depending on your URL paths, you may have to change the URL property of appreciateMe/script.php in the script.js JavaScript File.