BonBon Buttons are sweet CSS3 buttons that are sexy looking, really flexible, but with the most minimalistic markup as possible. There are 3 different materials. A “mate”, “glossy” and a “glass” version. The difference of the later two is that the glass version adds a dark blurry text-shadow which makes it look like you can see trough the button to its bottom.
However, BonBon Buttons are not meant to be used on your next project that targets the average internet user. He just wanted to show a couple techniques how to use some of the new CSS3/HTML5 features. So only the current version of Safari, Chrome and Firefox are supported.
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. Tutorialzine taught us how to implement An AJAX Click to Appreciate Badge, which you can include in every page of your website with a bit of jQuery magic.
AddyOsmani is going to teach us how to create a useful hover-based user interface using jQuery, CSS3, HTML5 and @font-face. Why a hover-based interface? With the popularity of touch-based web applications simplifying the way that people can use sites on mobile devices, there’s room for us to look into ways of making it even easier for people to use sites in desktop-based browsers too.
WanderWall achieves that and what it could easily be used for a portfolio or business site but the concepts we’ll learn could certainly be used to expand the idea further.
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.
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 '
'.$this->suggestion.'
'.(int)$this->rating.'
';
}
}
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.
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 = '
';
// 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 .='
';
}
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.
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.
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.
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
How does this FAQ section work?
With the help of jQuery and YQL, this script pulls the latest data ..
Can you modify it?
This is the best part of it - you can change the contents ..
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.
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.
We are using a single anchor tag for both the expand and the collapse button, by assigning it either the expand or the collapseCSS 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.
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 = $('
');
// 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('
'+
question+'
'+answer+'
');
});
// 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.