Creating A Web App From Scratch
Creating A Web App From Scratch
SCRATCH
Skullipso
Today we begin Part 1 of an 8-Part series on
building a web application from absolute scratch to
a complete product. I am going to kick things off by
introducing the idea, and then I will be handling the
design, UI, and general front-end stuff. We are
going to be going back and forth from here over to
my friend Jason Lengstorf’s blog Ennui Design.
Jason will be handling the back-end stuff like
application planning and database stuff. At the end
Home of the week, we’ll unleash the actual working
application for you. Here is the plan:
[Type the company address]
First of all, it needs to work and it needs to work well. That means good back end code that
does what it’s supposed to do and well. That means a good UI that is intuitive, helpful, and
pleasurable to use. It means keeping the app secure and users data private. None of these
things is trivial.
Through this whole 8-part series, we are going to create an app that hopefully does all
these things pretty well. We aren’t out to tell you this is the greatest app ever made, but
rather, we are going to use this app as a walk-through journey of the app creating process
and hopefully do as many smart things as we can along the way.
Sketch It Out
No need to get fancy right away. Here is a very rudimentary sketch of what the app might
look like:
Looks like a list to me. Each list item is a long rectangle, because the big idea here is to
colorize each list item, so putting them inside a colored box makes sense. There are some
interactive elements to the left and right of each list item. Those are going to be for
accomplishing the basic things we intent people to be able to do with their colored list. Lets
take a closer look.
Early UI Planning
We don’t necessarily want to be talking about specific technologies at this point, but we
should be thinking about how the UI will operate, so we can make choices about technology
that can accommodate our UI desires.
Click-to-edit
Drag and drop
Two-click delete
Automatic saving (after any action)
All this stuff basically adds up to a whole bunch of AJAX. We don’t want to load special
screens to do relatively trivial tasks like deleting a list item. That stuff should happen
seamlessly, smoothly and with proper feedback in response to mouse clicks without page
refreshes. In a sense, we are creating a one-page app, where the majority of interaction
with this app happens on a single page. This is certainly by design, and not trying to adhere
to any particular fad. Lists are easy and quick, that’s why are useful. If this app is
complicated, it’s usefulness is diminished and nobody will use it.
The Screens
Just doing some quick brainstorming of the idea so far, we can come up with quite a
number of “screens”, or states the application can be in.
Homepage
Logged out = Intro/Sales Page
Logged in = Your list
Log in page
Settings page
Lost password page
Account activation page
Emails
Yep, even emails should be considered a part of the “screens”, as they are a vital part of the
process and interaction with an app.
“Features”
People love “features”. Things that your app has that other apps don’t have, or that yours
does better. This is just as much for marketing as it is for your actual product. All the fancy
AJAX this app will have is certainly a feature, but that stuff these days is getting more and
more expected rather than a feature. The one feature that we will focus on with this app is
“public sharing”. Each list will have a unique URL that can be publicly shared. A visitor
visiting this URL can see the list in it’s exact current state, but not interact with it as far as
editing/adding/deleting.
Moving On
Now that we have the idea in place of what we want to build, in the next part we’ll dive into
looking at what this is going to take in terms of server-side technology.
Part II
Where We're At
Up to this point, we've planned the way our app is going to look, as well as given ourselves a
basic idea of how the app is going to function. The next step is to figure out what's going to
happen behind the scenes to allow our app to work the way we've planned.
Navigate to http://localhost/phpmyadmin and open the SQL tab. You can use the
GUI if you want, but we're going to use raw SQL commands for learning purposes. The database
will be named cl_db , which is all the information that is required to build the database.
However, we want to make sure that our users can use characters from any language in their
lists, so it's also a good idea to specify the collation and character set of the database. We'll be
using the UTF-8 character set with general collation , which supports multilingual characters
and is case-insensitive.
CREATE DATABASE `cl_db`
DEFAULT CHARACTER SET utf8
COLLATE utf8_general_ci;
Execute this command from the SQL tab in phpMyAdmin and the new database will become
available. Now that we've got a database, we're ready to build our tables.
Of course, we also need to store the user's email address, and in the interest of keeping
redundant data storage to a minimum, we'll assign each user a unique numeric identifier.
The MySQL command to build this table will look like this:
CREATE TABLE cl_db.users( UserID INT PRIMARY KEY AUTO_INCREME
NT, Username VARCHAR(150) NOT NULL, Password VARCHAR(150)
, ver_code VARCHAR(150), verified TINYINT DEFAULT 0)
To build this table, execute the following MySQL command in phpMyAdmin's SQL tab:
CREATE TABLE cl_db.lists( ListID INT PRIMARY KEY AUTO_INCREME
NT, UserID INT NOT NULL, ListURL VARCHAR(150))
Because great code starts with great organization, we'll be using an object-oriented approach.
Planning our PHP Classes
Object-oriented programming provides an easy way to keep related functions grouped together.
Afterlearning object-oriented programming , it becomes an incredibly powerful tool that increases
portability, readability, and usability of scripts. Our app is pretty simple, so we'll only need
two classes. The first class is going to handle user interactions, such as registering, updating
information, and logging in and out. The second class will handle list interactions, such as
adding, deleting, and moving list items.
Create an account
Verify the account
Update the account email address
Update the account password
Retrieve a forgotten password
Delete the account
In addition to those methods, we'll also need some support methods, such as one that will
send a verification email. We'll define these methods as we build the app in later installments of
this series.
Moving On
In our next installment of this series, we'll create the application workflow. Make sure you're
subscribed to CSS-Tricks so you don't miss out!
Part III
Developing a Workflow
We have a great start going on our list application at this point. The "big idea" is in place, we
know how we want the lists to be displayed and interacted with, and we have some back-end
structure in place to deal with users and all the data that goes along with these lists.
It was a good idea to start with the "meat" of the app, but there is a little bit more that goes into
a full application. Because we have users, that means we need a sign up form and a log in area
for returning users. Because users can be forgetful, we need a 'Lost Password'
feature. Because users should be just as concerned about security as we are, users need to be
able to change their passwords, change their login, and delete their accounts. Our one-
page app has just turned into a four or five page app, so we're going to need to think about
some workflow.
There will be two different states for the homepage: logged in and logged out. While logged
out, people need a way to sign in and to register, and this will be essentially the "sales" page
too, explaining the app. Logged in, the homepage will be the user's list itself. Logged in users
will also need to do some ancillary stuff related to their account, like change their email, change
their password, and delete their account, as well as a way to log out. These ancillary options are
probably best served on an account page. So now we are looking at at least two new pages:
Account Settings and Registration. Here is some flow:
This is the meat of our application so let's start here. The list is obviously the most important
thing, so let's keep the header small and keep the list front and center. List items are big
colored blocks with buttons for their associated actions nearby. Below the list a box for
entering new list items.
The home
page as it appears when logged in
When logged out, the homepage is going to act more like a "sales" page. Not that we plan to
charge for it, but just to explain and get people interested in using it. There isn't much to say
about a list app, so we'll keep it simple.
When logged out, we'll encourage the visitor to sign up
Small Bits
We've been designing long enough to know we might as well make the little buttons into a
separate file and keep them together as a sprite (a sprite is multiple images combined into one
to save HTTP requests, in our case, also the rollover states). So we'll do that and throw together
a favicon while we're at it.
Our intention with registration is going to be extremely simple. We're going to ask for a user's
email and that's it. They will be sent an email with a link in it to complete registration. The link
in that email will "activate" their account and they can choose the password at that time. So, our
registration page can be pretty darn simple.
As small as this is, this registration page sets the stage for other forms. We have a label/input
pair here that can be used for any input/pair in any of our site's forms.
Account
We'll use the same form design as the registration page here. It's not cheating or being lazy,
it's good design through consistency!
Account controls
Buttons
Notice the change in buttons. They are now big, orange and rounded. Much more button-like
don't you think? Again for consistency, let's make this the default for all buttons across the
site.
Moving on
The developer now has plenty to go on to start fleshing out the user interactions for the site. And
we have plenty to go on to start getting the HTML and CSS for all this ready, and ultimately to
AJAX this puppy up and get it working.
Part IV
It’s time to get our hands dirty with some markup!
We know we have a couple different pages to deal with here. The main page of course,
which acts as both our list page and sales page depending on login status. But then we have
sign in and sign up pages and account pages. So let’s be smart and work modularity. That
means we’ll make files like “header.php” and “close.php” that we can include on multiple
pages so we don’t have to repeat common code (e.g. the DOCTYPE, analytics code, and
ubiquitous things like that.
Header
<!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" />
<body>
<div id="page-wrap">
<div id="header">
<div id="control">
</div>
</div>
Right away in the header we’ve run across a few things where we need to be smart and
leave notes for the developer, but give him the things he needs. In the page title, we’ve left
a note to do something smart there. Different pages need differnet page titles, so clearly
something dynamic needs to happen there. Then with our control buttons (e.g. Account /
Logout) those buttons will be different depending on the logged in state of the user. So we’ll
just let the developer jump in there and make those things function correctly.
So at this point we have the top of a page. We are leaving the body, html, and #page-wrap
elements open, as beyond that is the main content of the page. Before we get into that
main content, let’s toss in the sidebar and footer areas so we have a complete skin.
Footer
Our design doesn’t call for much of a footer, so we’ll just close up those open elements and
add a note to put analytics here.
</div>
</body>
</html>
Sidebar
Our design calls for a bit of a sidebar. Right now, all we’ll use it for is a few notes on using
the application. But it’s nice to have some open room for content, as it’s extremely likely
that room will be needed for additional things as the app grows.
<div id="ribbon">
Reminders
<ul>
<li>Your list automatically saves</li>
<li>Double-click list items to edit them</li>
</ul>
</div>
Main Page
Now that we have our “modules” complete, let’s dig into a real page. The template for
building any page will be like this:
<div id="main">
</div>
<li class="colorBlue">
<span>Pick up dry cleaning</span>
<div class="draggertab tab"></div>
<div class="colortab tab"></div>
<div class="deletetab tab"></div>
<div class="donetab tab"></div>
</li>
<li class="colorGreen">
<span>Milk</span>
<div class="draggertab tab"></div>
<div class="colortab tab"></div>
<div class="deletetab tab"></div>
<div class="donetab tab"></div>
</li>
</ul>
The list itself will just be a regular ol’ unordered list. We’ll use CSS class names for the
colors. But then we need a bunch of controls for the list items. That’s what all those divs are
inside the list items. There are empty divs for dragging, changing color, deleting, and
checking off list items. We need these for the CSS so we can target them and style them.
We’re smart designers though, we know this markup is merely temporary. These lists will
be dynamically generated by the application. Just looking at all those empty control divs;
we know that those are probably automatically generated by the JavaScript. That’s fine, we
need the HTML in there now to set the stage and have everyone on the same page.
Why the spans inside the list items? Just being smart. Because the list items wrap more
than just the text, it’s likely we’ll need some kind of hook to target just the text later on.
Now we need to get an input on this page for adding new list items. Our developer will be
all over this, but we’ll put the basics in so we can style them.
<form action="" id="add-new">
<div>
<input type="text" id="new-list-item-text" name="new-list-item-text" />
<input type="submit" id="add-new-submit" value="Add" class="button" />
</div>
</form>
Then one of our applications features is having sharable public URL’s for our lists. Let’s put
that in here too.
<div id="share-area">
<p>Public list URL: <a href="#">URL GOES HERE</a>
<small>(Nobody but YOU will be able to edit this list)</small></p>
</div>
Ahhh, more work for the developer! But he’s ready for it. This public URL business leads us
into another possible scenario. We need this main page to be capable of displaying a list
without showing the input form or all the list controls. Basically you can just look at the list
but not interact with it. (Like if you wanted to send your mom your Christmas list!)
<li class="colorBlue">
<span>Pick up dry cleaning</span>
</li>
<li class="colorGreen">
<span>Milk</span>
</li>
</ul>
This will be exactly the same as the list above, only no control tabs, no form to add new
items, and no public URL area (hey, they are already here, what do they need the URL for).
We know this this probably will just be a change in how the backend code outputs the list.
But whatever, if we create this, everybody is on the same page.
Account Page
As a quick reminder, we us this structure for all pages, including this one.
<div id="main">
</div>
<?php
include_once "common/sidebar.php";
include_once "common/footer.php";
?>
That’s the beauty of working modularly, all common content is included so updates down
the line are much easier.
The account page is going to have several forms on it: one for updating email, one for
updating password, and a button for users to delete their accounts. Again, our developer
will be all over these forms filling them up with hidden inputs that pass along data and
adding in action URLs and methods and all that. We’ll leave that to him, but this gives us
enough to style.
<h2>Your Account</h2>
<form action="">
<div>
<input type="text" name="username" id="username" />
<label for="username">Change Email Address</label>
<hr />
<h2>Change Password</h2>
<form action="#">
<div>
<label for="password">New Password</label>
<input type="password" name="r" id="repeat-new-password" />
<hr />
The CSS
Reset
/*
RESET
*/
* { margin: 0; padding: 0; }
body { font: 14px/1.1 Helvetica, Sans-Serif; background: url(images/stripe.png) repeat-x; }
.clear { clear: both; }
img, a img { border: none; }
input { outline: none; }
Just getting things cleaned up.
Structure
/*
STRUCTURE
*/
body { font: 14px/1.1 Helvetica, Sans-Serif; background: url(images/stripe.png) repeat-x; }
#page-wrap { width: 960px; margin: 6px auto 50px; position: relative; }
hr { height: 1px; background: #ccc; clear: both; margin: 20px 0; border: none; display: block; }
Not too much complicated formatting for our little one-page app.
Typography
/*
TYPOGRAPHY
*/
a { text-decoration: none; color: #900; border-bottom: 1px dotted #900; outline: none; }
h1 { font: bold 36px Helvetica, Sans-Serif; margin: 0 0 8px 0; }
h2 { margin: 0 0 10px 0; }
p { margin: 0 0 6px 0; }
.button { background: url(/https/www.scribd.com/images/button-bg.png) repeat-x; -moz-border-radius: 5px; padding: 6px 12px; border: none; color:
white; cursor: pointer; text-shadow: 0 1px 1px #666; -webkit-border-radius: 5px; -webkit-box-shadow: 0 1px 3px #999; -moz-
box-shadow: 0 1px 3px #999; font: bold 16px Helvetica; }
.button:hover { background-position: bottom left; }
.red { background: red; color: white; font-size: 12px; padding: 3px; }
This isn’t really a content-based app, so we don’t have a whole heck of a lot of text
formatting. However we do have page headers, links, and buttons, so we’ll set those up
here.
Header
/*
HEADER
*/
#header { height: 68px; position: relative; }
#header h1 { position: absolute; top: 0; left: 0; z-index: 2; text-indent: -9999px; overflow: hidden; }
#header h1 a { display: block; text-indent: -9999px; width: 200px; height: 38px; border: none; background: url(/https/www.scribd.com/images/logo.png)
no-repeat; }
#control { width: 500px; float: right; padding: 10px 237px 0 0; text-align: right; }
Our little stripe header doesn’t take much. Just a little CSS image replacement for the logo
and placement of our control buttons.
Lists
/*
LISTS
*/
#list { list-style: none; }
#list li { position: relative; margin: 0 0 8px 0; padding: 0 0 0 70px; width: 607px; }
#list li span { padding: 8px; -moz-border-radius: 5px; -webkit-border-radius: 5px; width: 589px; display: block; position:
relative; }
.colorBlue span { background: rgb(115, 184, 191); }
.colorYellow span { background: rgb(255, 255, 255); }
.colorRed span { background: rgb(187, 49, 47); color: white; }
.colorGreen span { background: rgb(145, 191, 75); }
.tab { background: url(images/minibuttons.png) no-repeat; height: 21px; top: 4px; }
.draggertab { position: absolute; left: 0px; width: 31px; cursor: move; }
.draggertab:hover { background-position: 0 -21px; }
.colortab { position: absolute; left: 34px; width: 34px; background-position: -31px 0; cursor: pointer; }
.colortab:hover { background-position: -31px -21px; }
.deletetab { position: absolute; right: -35px; width: 15px; background-position: -82px 0; cursor: pointer; }
.deletetab:hover { background-position: -82px -21px; }
.donetab { position: absolute; right: -17px; width: 16px; background-position: -65px 0; cursor: pointer; }
.donetab:hover { background-position: -65px -21px; }
.crossout { position: absolute; top: 50%; left: 0; height: 1px; }
A lot more stuff needed here. Here we’ll set up how the lists look: the colors, the spacing,
the rounded corners, etc. We’ll also position all the little helper controls and give them
appropriate backgrounds. Notice only a single image is used, minibuttons.png. A single CSS
Sprite for mad efficiency!
Forms
/*
FORM STUFF
*/
label { background: #999; color: white; padding: 3px; }
input[type="text"], input[type="password"] { width: 324px; border: 3px solid #999; font-size: 18px; padding: 7px; display:
block; }
#add-new input[type="text"] { width: 532px; float: left; margin: 0 10px 0 69px; }
#add-new input[type="text"]:focus { border-color: #73B8BF; }
#add-new input[type="submit"] { padding: 10px 12px; }
ul#list li span input[style] { width: 90% !important; }
Forms across our whole site will be the same, so we set that up here. The one exception is
the “Add New” area on our lists, which is basically the same as any other input except
bigger and is floated to the left to accommodate the “Add” button. Since we plan to use
click-to-edit, the list items temporarily turn into text inputs when doing that, so we’ll plan
for that by shortening the length of them to accommodate for a “Save” button.
Messaging
/*
MESSAGING
*/
.message { padding: 10px; margin: 0 0 10px 0; width: 607px; }
.good { background: #9ff5b6; }
.bad { color: #ef0040; }
We haven’t talked too much about error messaging, but we can assume that because this is
a web app, there will be some of it (for example, you enter in a wrong password, your
passwords don’t match, you have successfully done something, etc). We’ll set up one class
for messages in general and then classes for good and bad versions.
Sidebar
/*
SIDEBAR
*/
#ribbon { position: absolute; right: 0; width: 125px; padding: 60px 30px 0 47px; height: 756px; top: -6px; background:
url(/images/ribbon-bg.png) no-repeat; }
#ribbon ul { list-style: none; }
#ribbon ul li { background: rgba(0,0,0,0.8); color: white; padding: 5px; margin: 0 0 5px 0; font-size: 12px; }
Just some simple stuff for our list of reminders.
Moving Along
Our developer now has plenty to work with to make this app functional. Once he’s gotten a
good start on that, we’ll tackle AJAX and making all those list interactions happen almost
like a desktop app.
Part V
We'll focus this installment of the series on the user's account interactions. These include:
Creating an Account
Modifying Account Information
Resetting a Lost Password
Deleting an Account
Our site won't require many constants , but in the interest of keeping them easy to maintain,
we'llcreate a separate file to contain any information that is site-wide. This will be
called constants.inc.php , and it will reside in a new folder called inc — this folder will
contain our PHP classes as well.
Creating a constants file is a good idea for pieces of information that will be used often and in
different scopes throughout a site. That way, if your database changes, you're able to change
every database connection simply by swapping out the information in one file.
<?php
// Database credentials
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'cl_db');
?>
As we develop, we'll add more to this file.
Next, we want to create a connection so that our application can communicate with our
database.This file will reside in the common folder along with the header, footer, and sidebar
files. This file will create a database connection using PDO (PHP Data Objects) , as well as
setting up a couple other site-wide features: error reporting and opening a session.
The file will look like this when all's said and done:
<?php
// Set the error reporting level
error_reporting(E_ALL);
ini_set("display_errors", 1);
// Start a PHP session
session_start();
// Include site constants
include_once "inc/constants.inc.php";
// Create a database object
try {
$dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME;
$db = new PDO($dsn, DB_USER, DB_PASS);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
exit;
}
?>
Because we're in the development stage, we want to see any and every error that occurs on
the site. By setting error_reporting() to E_ALL and changing
the display_errors directive to 1 using ini_set() , we ensure that even notices will be
displayed, which will keep our code cleaner and more secure.
Next, we use session_start() to start a PHP session. This will allow our users to stay
logged in when we build that functionality later.
Finally, we include config.inc.php and create a PDO object using the constants
defined within it. Note the use of the try-catch statement—this gives us the ability to
use Exceptions , which help improve error handling. In this case, if the database connection fails,
we're simply going to output the error message.
Why PDO?
The reason we're using PDO for this project is because of its support for prepared
statements ,which virtually eliminates the risk of SQL injection . There are other options that
allow prepared statements, such as the MySQLi extension . However, PDO is not database-
specific, so migrating the app to Oracle or PostgreSQL wouldn't require a full rewrite of our
code.
Also, having used both MySQLi and PDO in projects, it's just my personal preference to use
PDO. Feel free to use whatever method of connecting to the database you prefer, but keep in
mind that all database interactions in this exercise are assuming the use of PDO, and as such
will probably require some reworking to accommodate your changes.
<?php
/**
* Handles user interactions within the app
*
* PHP version 5
*
* @author Jason Lengstorf
* @author Chris Coyier
* @copyright 2009 Chris Coyier and Jason Lengstorf
* @license http://www.opensource.org/licenses/mit-license.html MI
T License
*
*/
class ColoredListsUsers
{
}
?>
class ColoredListsUsers
{
/**
* The database object
*
* @var object
*/
private $_db;
/**
* Checks for a database object and creates one if none is found
*
* @param object $db
* @return void
*/
public function __construct($db=NULL)
{
if(is_object($db))
{
$this->_db = $db;
}
else
{
$dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME;
$this->_db = new PDO($dsn, DB_USER, DB_PASS);
}
}
}
Now we are able to create an instance of our ColoredListsUsers object and use it to
communicate with our database. Next, let's start building user interactions!
Creating an Account
First and foremost, a user needs to be able to create an account. This will give them access to
the rest of the site's functionality.
As it stands, when a user visits our app, they're greeted with our "sales" page, which encourages
them to click the "Sign Up" button in the top right of their screen:
<?php
include_once "common/base.php";
$pageTitle = "Register";
include_once "common/header.php";
if(!empty($_POST['username'])):
include_once "inc/class.users.inc.php";
$users = new ColoredListsUsers($db);
echo $users->createAccount();
else:
?>
<h2>Sign up</h2>
<form method="post" action="signup.php" id="registerform">
<div>
<label for="username">Email:</label>
<input type="text" name="username" id="username" /><b
r />
<input type="submit" name="register" id="register" va
lue="Sign up" />
</div>
</form>
<?php
endif;
include_once 'common/close.php';
?>
To start, we include our common/base.php and common/header.php files. Also, notice
that we're declaring a variable called $pageTitle just before we include the header.
Remember in Part 4 when we built the header file and left that comment in the title tag?
<title>Colored Lists | <!-- Do Something Smart Here --></title>
We're going to replace that with a snippet of PHP that reads:
<title>Colored Lists | <?php echo $pageTitle ?></title>
That gives us the opportunity to post a different title for each page of our app.
With the proper files included, we can then create our sign-up form. The form will submit
to signup.php —itself—so we need to place an if-else check to see if the form has been
submitted. If so, we create a new ColoredListsUsers object and call
the createAccount() method (which we'll write in the next section).
Finally, we close the if-else statement and include the footer. Our sign-up page should look
like this:
Notice the use of alternative syntax for the if-else statement. Normally, I don't like to use
this format, but in the case of outputting HTML, I prefer the way it ends with endif; instead of
a closing curly brace ( } ), which helps with readability in the script.
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Checks and inserts a new account email into the database
*
* @return string a message indicating the action status
*/
public function createAccount()
{
$u = trim($_POST['username']);
$v = sha1(time());
$sql = "SELECT COUNT(Username) AS theCount
FROM users
WHERE Username=:email";
if($stmt = $this->_db->prepare($sql)) {
$stmt->bindParam(":email", $u, PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch();
if($row['theCount']!=0) {
return "<h2> Error </h2>"
. "<p> Sorry, that email is already in use. "
. "Please try again. </p>";
}
if(!$this->sendVerificationEmail($u, $v)) {
return "<h2> Error </h2>"
. "<p> There was an error sending your"
. " verification email. Please "
. "<a href=\"mailto:[email protected]\">conta
ct "
. "us</a> for support. We apologize for the "
. "inconvenience. </p>";
}
$stmt->closeCursor();
}
$sql = "INSERT INTO users(Username, ver_code)
VALUES(:email, :ver)";
if($stmt = $this->_db->prepare($sql)) {
$stmt->bindParam(":email", $u, PDO::PARAM_STR);
$stmt->bindParam(":ver", $v, PDO::PARAM_STR);
$stmt->execute();
$stmt->closeCursor();
$userID = $this->_db->lastInsertId();
$url = dechex($userID);
/*
* If the UserID was successfully
* retrieved, create a default list.
*/
$sql = "INSERT INTO lists (UserID, ListURL)
VALUES ($userID, $url)";
if(!$this->_db->query($sql)) {
return "<h2> Error </h2>"
. "<p> Your account was created, but "
. "creating your first list failed. </p>";
} else {
return "<h2> Success! </h2>"
. "<p> Your account was successfully "
. "created with the username <strong>$u</strong>.
"
. " Check your email!";
}
} else {
return "<h2> Error </h2><p> Couldn't insert the "
. "user information into the database. </p>";
}
}
}
This method follows several steps to create an account: first, it retrieves the posted email
address from the form (stored in the $_POST superglobal ) and generates a hard-to-guess
verification code (the SHA1 hash of the current timestamp); second, it makes sure the supplied
email address isn't already in use; third, it generates and sends a verification email to the
user with instructions on how to verify their account (we'll define the method that does this in the
next section); fourth, it stores the email address and verification code in the database; and
finally, it creates a list for the user.
Each of these steps is monitored, and if any of them should fail, a specific error message is
generated. Upon success, a message is generated to let the user know they should expect an
email.
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Sends an email to a user with a link to verify their new accou
nt
*
* @param string $email The user's email address
* @param string $ver The random verification code for the use
r
* @return boolean TRUE on successful send and FALSE on fa
ilure
*/
private function sendVerificationEmail($email, $ver)
{
$e = sha1($email); // For verification purposes
$to = trim($email);
$subject = "[Colored Lists] Please Verify Your Account";
$headers = <<<MESSAGE
From: Colored Lists <[email protected]>
Content-Type: text/plain;
MESSAGE;
$msg = <<<EMAIL
You have a new account at Colored Lists!
To get started, please activate your account and choose a
password by following the link below.
Your Username: $email
Activate your account: http://coloredlists.com/accountverify.php?
v=$ver&e=$e
If you have any questions, please contact [email protected].
--
Thanks!
Chris and Jason
www.ColoredLists.com
EMAIL;
return mail($to, $subject, $msg, $headers);
}
}
The most important part of this method is the activation
link, http://coloredlists.com/accountverify.php?v=$ver&e=$e . This
link sends the user to our app's account verification file (which we'll write in the next
step) and sends the user's hashed email address along with their verification code in the
URI. This will allow us to identify and verify the user when they follow the link.
First, let's create a new file called accountverify.php in the root level of our app. Inside,
place the following code:
<?php
include_once "common/base.php";
$pageTitle = "Verify Your Account";
include_once "common/header.php";
if(isset($_GET['v']) && isset($_GET['e']))
{
include_once "inc/class.users.inc.php";
$users = new ColoredListsUsers($db);
$ret = $users->verifyAccount();
}
elseif(isset($_POST['v']))
{
include_once "inc/class.users.inc.php";
$users = new ColoredListsUsers($db);
$ret = $users->updatePassword();
}
else
{
header("Location: /signup.php");
exit;
}
if(isset($ret[0])):
echo isset($ret[1]) ? $ret[1] : NULL;
if($ret[0]<3):
?>
<h2>Choose a Password</h2>
<form method="post" action="accountverify.php">
<div>
<label for="p">Choose a Password:</label>
<input type="password" name="p" id="p" /><br />
<label for="r">Re-Type Password:</label>
<input type="password" name="r" id="r" /><br />
<input type="hidden" name="v" value="<?php echo $_GE
T['v'] ?>" />
<input type="submit" name="verify" id="verify" value=
"Verify Your Account" />
</div>
</form>
<?php
endif;
else:
echo '<meta http-equiv="refresh" content="0;/">';
endif;
include_once("common/ads.php");
include_once 'common/close.php';
?>
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Checks credentials and verifies a user account
*
* @return array an array containing a status code and status
message
*/
public function verifyAccount()
{
$sql = "SELECT Username
FROM users
WHERE ver_code=:ver
AND SHA1(Username)=:user
AND verified=0";
if($stmt = $this->_db->prepare($sql))
{
$stmt->bindParam(':ver', $_GET['v'], PDO::PARAM_STR);
$stmt->bindParam(':user', $_GET['e'], PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch();
if(isset($row['Username']))
{
// Logs the user in if verification is successful
$_SESSION['Username'] = $row['Username'];
$_SESSION['LoggedIn'] = 1;
}
else
{
return array(4, "<h2>Verification Error</h2>\n"
. "<p>This account has already been verified. "
. "Did you <a href=\"/password.php\">forget "
. "your password?</a>");
}
$stmt->closeCursor();
// No error message is required if verification is succes
sful
return array(0, NULL);
}
else
{
return array(2, "<h2>Error</h2>\n<p>Database error.</p>")
;
}
}
}
This method executes a query that loads the user name stored in the database with the
verification code, hashed user name, and a verified status of 0 . If a user name is returned, login
credentials are stored. This method returns an array with an error code in the first index,
and a message in the second. The error code 0 means nothing went wrong.
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Changes the user's password
*
* @return boolean TRUE on success and FALSE on failure
*/
public function updatePassword()
{
if(isset($_POST['p'])
&& isset($_POST['r'])
&& $_POST['p']==$_POST['r'])
{
$sql = "UPDATE users
SET Password=MD5(:pass), verified=1
WHERE ver_code=:ver
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":pass", $_POST['p'], PDO::PARAM_STR
);
$stmt->bindParam(":ver", $_POST['v'], PDO::PARAM_STR)
;
$stmt->execute();
$stmt->closeCursor();
return TRUE;
}
catch(PDOException $e)
{
return FALSE;
}
}
else
{
return FALSE;
}
}
}
Finally, since verifying an account logs a user in, we need to update common/header.php to
recognize that a user is logged in and display different options. In Part
4 , common/header.php featured a code snippet that looked like this:
<!-- IF LOGGED IN -->
<p><a href="/logout.php" class="button">Log out</a> <
a href="/account.php" class="button">Your Account</a></p>
<!-- IF LOGGED OUT -->
<p><a class="button" href="/signup.php">Sign up</a> &
nbsp; <a class="button" href="/login.php">Log in</a></p>
<!-- END OF IF STATEMENT -->
To make those comments into functional code, we need to modify this snippet with an if-else
block:
<?php
if(isset($_SESSION['LoggedIn']) && isset($_SESSION['Username'])
&& $_SESSION['LoggedIn']==1):
?>
<p><a href="/logout.php" class="button">Log out</a> <
a href="/account.php" class="button">Your Account</a></p>
<?php else: ?>
<p><a class="button" href="/signup.php">Sign up</a> &
nbsp; <a class="button" href="/login.php">Log in</a></p>
<?php endif; ?>
Notice that we store in the session both the user name ( $_SESSION['Username'] ) and a
flag that tells us if the user is logged in ( $_SESSION['LoggedIn'] ).
Logging In
Next, let's build the login form and allow our user to log in. To start, let's create a new file
named login.php at the root level of our app. Like our other publicly displayed files, this will
include the base and header files. Then it checks if a user is already logged in, if the login form
was submitted, or if the user needs to log in.
If logged in, the user is notified of this fact and asked if he or she wishes to log out.
If the form has been submitted, a new ColoredListsUsers object is created and
the accountLogin() method is called. If the login succeeds, the user is directed to the home
page, where his or her list will appear; otherwise, the login form is displayed again with an error.
Finally, the sidebar ads and footer are included to round out the file.
When the file is all put together, it should look like this:
<?php
include_once "common/base.php";
$pageTitle = "Home";
include_once "common/header.php";
if(!empty($_SESSION['LoggedIn']) && !
empty($_SESSION['Username'])):
?>
<p>You are currently <strong>logged in.</strong></p>
<p><a href="/logout.php">Log out</a></p>
<?php
elseif(!empty($_POST['username']) && !empty($_POST['password'])):
include_once 'inc/class.users.inc.php';
$users = new ColoredListsUsers($db);
if($users->accountLogin()===TRUE):
echo "<meta http-equiv='refresh' content='0;/'>";
exit;
else:
?>
<h2>Login Failed—Try Again?</h2>
<form method="post" action="login.php" name="loginform" id="l
oginform">
<div>
<input type="text" name="username" id="username" />
<label for="username">Email</label>
<br /><br />
<input type="password" name="password" id="password"
/>
<label for="password">Password</label>
<br /><br />
<input type="submit" name="login" id="login" value="L
ogin" class="button" />
</div>
</form>
<p><a href="/password.php">Did you forget your password?
</a></p>
<?php
endif;
else:
?>
<h2>Your list awaits...</h2>
<form method="post" action="login.php" name="loginform" id="l
oginform">
<div>
<input type="text" name="username" id="username" />
<label for="username">Email</label>
<br /><br />
<input type="password" name="password" id="password"
/>
<label for="password">Password</label>
<br /><br />
<input type="submit" name="login" id="login" value="L
ogin" class="button" />
</div>
</form><br /><br />
<p><a href="/password.php">Did you forget your password?
</a></p>
<?php
endif;
?>
<div style="clear: both;"></div>
<?php
include_once "common/ads.php";
include_once "common/close.php";
?>
Notice the "Did you forget your password?" links — we'll be building this functionality a little later
on in the article.
Build this method in ColoredListsUsers by inserting the following code:
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Checks credentials and logs in the user
*
* @return boolean TRUE on success and FALSE on failure
*/
public function accountLogin()
{
$sql = "SELECT Username
FROM users
WHERE Username=:user
AND Password=MD5(:pass)
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':user', $_POST['username'], PDO::PARAM_
STR);
$stmt->bindParam(':pass', $_POST['password'], PDO::PARAM_
STR);
$stmt->execute();
if($stmt->rowCount()==1)
{
$_SESSION['Username'] = htmlentities($_POST['username
'], ENT_QUOTES);
$_SESSION['LoggedIn'] = 1;
return TRUE;
}
else
{
return FALSE;
}
}
catch(PDOException $e)
{
return FALSE;
}
}
}
Logging Out
Next, our user needs to be able to log out. This is as easy as destroying the login data stored in
the session and sending the user back to the login page.
Create a new file named logout.php at the root level of the app and place the following code
inside:
<?php
session_start();
unset($_SESSION['LoggedIn']);
unset($_SESSION['Username']);
?>
<meta http-equiv="refresh" content="0;login.php">
Create a file named account.php at the root level of the app. There's a lot going on here
because we're essentially combining three app functions within one file.
First, we include the base file and check that the user is logged in. If not, he or she gets sent
out to the main page.
If the user is logged in, we check if any actions have already been attempted and assemble
the corresponding success or failure messages if any are found.
Finally, we include the sidebar ads and the footer. Altogether, the file should look like this:
<?php
include_once "common/base.php";
if(isset($_SESSION['LoggedIn']) && $_SESSION['LoggedIn']==1):
$pageTitle = "Your Account";
include_once "common/header.php";
include_once 'inc/class.users.inc.php';
$users = new ColoredListsUsers($db);
if(isset($_GET['email']) && $_GET['email']=="changed")
{
echo "<div class='message good'>Your email address "
. "has been changed.</div>";
}
else if(isset($_GET['email']) && $_GET['email']=="failed")
{
echo "<div class='message bad'>There was an error "
. "changing your email address.</div>";
}
if(isset($_GET['password']) && $_GET['password']=="changed")
{
echo "<div class='message good'>Your password "
. "has been changed.</div>";
}
elseif(isset($_GET['password']) && $_GET['password']=="nomatc
h")
{
echo "<div class='message bad'>The two passwords "
. "did not match. Try again!</div>";
}
if(isset($_GET['delete']) && $_GET['delete']=="failed")
{
echo "<div class='message bad'>There was an error "
. "deleting your account.</div>";
}
list($userID, $v) = $users->retrieveAccountInfo();
?>
<h2>Your Account</h2>
<form method="post" action="db-interaction/users.php">
<div>
<input type="hidden" name="userid"
value="<?php echo $userID ?>" />
<input type="hidden" name="action"
value="changeemail" />
<input type="text" name="username" id="username" />
<label for="username">Change Email Address</label>
<br /><br />
<input type="submit" name="change-email-submit"
id="change-email-submit" value="Change Email"
class="button" />
</div>
</form><br /><br />
<form method="post" action="db-interaction/users.php"
id="change-password-form">
<div>
<input type="hidden" name="user-id"
value="<?php echo $userID ?>" />
<input type="hidden" name="v"
value="<?php echo $v ?>" />
<input type="hidden" name="action"
value="changepassword" />
<input type="password"
name="p" id="new-password" />
<label for="password">New Password</label>
<br /><br />
<input type="password" name="r"
id="repeat-new-password" />
<label for="password">Repeat New Password</label>
<br /><br />
<input type="submit" name="change-password-submit"
id="change-password-submit" value="Change Passwor
d"
class="button" />
</div>
</form>
<hr />
<form method="post" action="deleteaccount.php"
id="delete-account-form">
<div>
<input type="hidden" name="user-id"
value="<?php echo $userID ?>" />
<input type="submit"
name="delete-account-submit" id="delete-account-
submit"
value="Delete Account?" class="button" />
</div>
</form>
<?php
else:
header("Location: /");
exit;
endif;
?>
<div class="clear"></div>
<?php
include_once "common/ads.php";
include_once "common/close.php";
?>
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Retrieves the ID and verification code for a user
*
* @return mixed an array of info or FALSE on failure
*/
public function retrieveAccountInfo()
{
$sql = "SELECT UserID, ver_code
FROM users
WHERE Username=:user";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':user', $_SESSION['Username'], PDO::PAR
AM_STR);
$stmt->execute();
$row = $stmt->fetch();
$stmt->closeCursor();
return array($row['UserID'], $row['ver_code']);
}
catch(PDOException $e)
{
return FALSE;
}
}
}
This file will be placed in a new folder called db-interaction , and it will be
named users.php . Place the following code in the new file:
<?php
session_start();
include_once "../inc/constants.inc.php";
include_once "../inc/class.users.inc.php";
$userObj = new ColoredListsUsers();
if(!empty($_POST['action'])
&& isset($_SESSION['LoggedIn'])
&& $_SESSION['LoggedIn']==1)
{
switch($_POST['action'])
{
case 'changeemail':
$status = $userObj->updateEmail() ? "changed" : "failed";
header("Location: /account.php?email=$status");
break;
case 'changepassword':
$status = $userObj->updatePassword() ? "changed" : "nomat
ch";
header("Location: /account.php?password=$status");
break;
case 'deleteaccount':
$userObj->deleteAccount();
break;
default:
header("Location: /");
break;
}
}
elseif($_POST['action']=="resetpassword")
{
if($resp=$userObj->resetPassword()===TRUE)
{
header("Location: /resetpending.php");
}
else
{
echo $resp;
}
exit;
}
else
{
header("Location: /");
exit;
}
?>
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Changes a user's email address
*
* @return boolean TRUE on success and FALSE on failure
*/
public function updateEmail()
{
$sql = "UPDATE users
SET Username=:email
WHERE UserID=:user
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':email', $_POST['username'], PDO::PARAM
_STR);
$stmt->bindParam(':user', $_POST['userid'], PDO::PARAM_IN
T);
$stmt->execute();
$stmt->closeCursor();
// Updates the session variable
$_SESSION['Username'] = htmlentities($_POST['username'],
ENT_QUOTES);
return TRUE;
}
catch(PDOException $e)
{
return FALSE;
}
}
}
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Changes the user's password
*
* @return boolean TRUE on success and FALSE on failure
*/
public function updatePassword()
{
if(isset($_POST['p'])
&& isset($_POST['r'])
&& $_POST['p']==$_POST['r'])
{
$sql = "UPDATE users
SET Password=MD5(:pass), verified=1
WHERE ver_code=:ver
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":pass", $_POST['p'], PDO::PARAM_STR
);
$stmt->bindParam(":ver", $_POST['v'], PDO::PARAM_STR)
;
$stmt->execute();
$stmt->closeCursor();
return TRUE;
}
catch(PDOException $e)
{
return FALSE;
}
}
else
{
return FALSE;
}
}
}
The method, when it's all written, will look like this:
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Deletes an account and all associated lists and items
*
* @return void
*/
public function deleteAccount()
{
if(isset($_SESSION['LoggedIn']) && $_SESSION['LoggedIn']==1)
{
// Delete list items
$sql = "DELETE FROM list_items
WHERE ListID=(
SELECT ListID
FROM lists
WHERE UserID=:user
LIMIT 1
)";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":user", $_POST['user-id'], PDO::PAR
AM_INT);
$stmt->execute();
$stmt->closeCursor();
}
catch(PDOException $e)
{
die($e->getMessage());
}
// Delete the user's list(s)
$sql = "DELETE FROM lists
WHERE UserID=:user";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":user", $_POST['user-id'], PDO::PAR
AM_INT);
$stmt->execute();
$stmt->closeCursor();
}
catch(PDOException $e)
{
die($e->getMessage());
}
// Delete the user
$sql = "DELETE FROM users
WHERE UserID=:user
AND Username=:email";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":user", $_POST['user-id'], PDO::PAR
AM_INT);
$stmt->bindParam(":email", $_SESSION['Username'], PDO
::PARAM_STR);
$stmt->execute();
$stmt->closeCursor();
}
catch(PDOException $e)
{
die($e->getMessage());
}
// Destroy the user's session and send to a confirmation
page
unset($_SESSION['LoggedIn'], $_SESSION['Username']);
header("Location: /gone.php");
exit;
}
else
{
header("Location: /account.php?delete=failed");
exit;
}
}
}
<?php
include_once "common/base.php";
$pageTitle = "Reset Your Password";
include_once "common/header.php";
?>
<h2>Reset Your Password</h2>
<p>Enter the email address you signed up with and we'll send
you a link to reset your password.</p>
<form action="db-interaction/users.php" method="post">
<div>
<input type="hidden" name="action"
value="resetpassword" />
<input type="text" name="username" id="username" />
<label for="username">Email</label><br /><br />
<input type="submit" name="reset" id="reset"
value="Reset Password" class="button" />
</div>
</form>
<?php
include_once "common/ads.php";
include_once "common/close.php";
?>
When a user visits this page, they'll be able to enter their email address. Submitting the form will
return the account to unverified and send the user an email with a link to reset their
password.
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Resets a user's status to unverified and sends them an email
*
* @return mixed TRUE on success and a message on failure
*/
public function resetPassword()
{
$sql = "UPDATE users
SET verified=0
WHERE Username=:user
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(":user", $_POST['username'], PDO::PARAM_
STR);
$stmt->execute();
$stmt->closeCursor();
}
catch(PDOException $e)
{
return $e->getMessage();
}
// Send the reset email
if(!$this->sendResetEmail($_POST['username'], $v))
{
return "Sending the email failed!";
}
return TRUE;
}
}
class ColoredListsUsers
{
// Class properties and other methods omitted to save space
/**
* Sends a link to a user that lets them reset their password
*
* @param string $email the user's email address
* @param string $ver the user's verification code
* @return boolean TRUE on success and FALSE on failure
*/
private function sendResetEmail($email, $ver)
{
$e = sha1($email); // For verification purposes
$to = trim($email);
$subject = "[Colored Lists] Request to Reset Your Password";
$headers = <<<MESSAGE
From: Colored Lists <[email protected]>
Content-Type: text/plain;
MESSAGE;
$msg = <<<EMAIL
We just heard you forgot your password! Bummer! To get going again,
head over to the link below and choose a new password.
Follow this link to reset your password:
http://coloredlists.com/resetpassword.php?v=$ver&e=$e
If you have any questions, please contact [email protected].
--
Thanks!
Chris and Jason
www.ColoredLists.com
EMAIL;
return mail($to, $subject, $msg, $headers);
}
}
If so, we are able to use the verifyAccount() method we wrote earlier to ensure that their
credentials are correct. After verifying their credentials, we display a form that allows them to
choose a password and confirm it.
After submitting the form, our script will fire the updatePassword() method we created
earlier to save the new password. Then we redirect the user to account.php , where they're
shown a confirmation message letting them know that their password was changed.
<?php
include_once "common/base.php";
if(isset($_GET['v']) && isset($_GET['e']))
{
include_once "inc/class.users.inc.php";
$users = new ColoredListsUsers($db);
$ret = $users->verifyAccount();
}
elseif(isset($_POST['v']))
{
include_once "inc/class.users.inc.php";
$users = new ColoredListsUsers($db);
$status = $users->updatePassword() ? "changed" : "failed";
header("Location: /account.php?password=$status");
exit;
}
else
{
header("Location: /login.php");
exit;
}
$pageTitle = "Reset Your Password";
include_once "common/header.php";
if(isset($ret[0])):
echo isset($ret[1]) ? $ret[1] : NULL;
if($ret[0]<3):
?>
<h2>Reset Your Password</h2>
<form method="post" action="accountverify.php">
<div>
<label for="p">Choose a New Password:</label>
<input type="password" name="p" id="p" /><br />
<label for="r">Re-Type Password:</label>
<input type="password" name="r" id="r" /><br />
<input type="hidden" name="v" value="<?php echo $_GE
T['v'] ?>" />
<input type="submit" name="verify" id="verify" value=
"Reset Your Password" />
</div>
</form>
<?php
endif;
else:
echo '<meta http-equiv="refresh" content="0;/">';
endif;
include_once("common/ads.php");
include_once 'common/close.php';
?>
Moving On...
This article covered a whole lot of ground, and I went over some of it pretty quickly. Please
don't hesitate to ask for clarification in the comments!
In the next part of this series, our front-end designer will use some dummy lists to
create AJAX effects. After he's finished with the dummy lists, we'll explore how to combine those
AJAX effects with our back-end and build the list interactions class in part 7.
Part VI
Our developer has already done a massive amount of work turning this idea into a real
application. Now let’s make some more for him! The most important part of this app is
creating and managing your list. We decided from the get-go that this was going to be an
AJAX-y app. We didn’t chose AJAX because it’s a popular buzzword, we chose it because we
know it’s the best path toward making a responsive, easy to use, natural feeling application
on the web.
Interface JavaScript
Alongside all that AJAX saving is all the stuff that makes the interface do what visually it’s
saying it will do. That little drag tab is saying it can drag list items up and down. We are
saying after that happens we are going to save the list. But how does that actually work?
Don’t worry, we’ll get to it. For now let’s think through all the interface JavaScript things
that we need:
Click and drag the drag tab, list items can be dragged around and reordered.
Click the color tab, the list items color is toggled between some predefined choices.
Click the checkmark, the list item crosses out and and fades down.
Click the X, a confirmation slides out. Click again, list item is whisked away.
Double-click the list item, text turns into a text-input for editing.
Type in the large box below and click add, new list item is appended to bottom of the
list.
And again, we’re going to get to all that. Just a little more setup to do!
function initialize() {
};
The first thing we are going to do there is clean up the markup a bit by inserting common
elements with JavaScript rather than have them directly in the markup. Remember what the
markup for the list looked like when we mocked it up?
<ul id="list">
<li class="colorRed">
<span>Walk the dog</span>
<div class="draggertab tab"></div>
<div class="colortab tab"></div>
<div class="deletetab tab"></div>
<div class="donetab tab"></div>
</li>
</ul>
Much of that is redundant across all the list items. What we want is more like this:
<ul id="list">
<li class="colorRed">
Walk the dog
</li>
<!-- more list items -->
</ul>
All the divs and the span has been removed. The class name on the list item is fine, because
PHP will be spitting that out for us when it reads the database and outputs the list.
How do we append all that extra HTML? Easy with jQuery. Target the list items and wrap
each of the the innards using the wrapInner() function and append the extra divs with the
append() function.
function initialize() {
};
Bind events to the new functionality tabs, the smart way
Binding an event to an HTML element is pretty easy in JavaScript. It’s like this:
$("li").click(function() {
// do something
});
There is nothing wrong with that, but we are in a bit of a unique situation with our list app
here. When you bind events like that 1) it creates a unique event handler for every single
list item on the page, each one taking up browser memory and 2) it only does it once, for
the current state of the DOM. Don’t worry about all that too much, the point is binding
events this way isn’t ideal for us because we will be inserting new list items dynamically.
When a user inserts a new list item, that gets plugged into the DOM right then and there.
That new list item will not be bound as the others are, meaning all those fancy little tabs
won’t work right. Boo hoo. What can we do to solve that? Well we can create a new function
that will be called when the page loads and when new list items are appended that does all
that event binding work. That will definitely do the trick, but… jQuery is smarter than that.
jQuery provides a function called live() that eliminates this problem entirely.
$("li").live("click", function() {
// do something
});
Binding events with the live() function is fantastic for us because 1) it only creates one
event handler which is far more efficient and 2) new items appended to the page are
automatically bound by the same handler. Killer.
So for our little functionality tabs, we’ll be using them like this:
$(".donetab").live("click", function() {
// do stuff
});
$(".colortab").live("click", function(){
// do stuff
});
$(".deletetab").live("click", function(){
// do stuff
});
The drag tab doesn’t have click event, it’s actually going to use jQuery UI’s draggable
functionality to do it’s thing. Let’s check that out.
$("#list").sortable({
handle : ".draggertab",
update : function(event, ui){
// Developer, this function fires after a list sort, commence list saving!
},
forcePlaceholderSize: true
});
$(".donetab").live("click", function() {
if(!$(this).siblings('span').children('img.crossout').length) {
$(this)
.parent()
.find("span")
.append("<img src='/images/crossout.png' class='crossout' />")
.find(".crossout")
.animate({
width: "100%"
})
.end()
.animate({
opacity: "0.5"
},
"slow",
"swing",
function() {
// DEVELOPER, the user has marked this item as done, commence saving!
})
}
else
{
$(this)
.siblings('span')
.find('img.crossout')
.remove()
.end()
.animate({
opacity : 1
},
"slow",
"swing",
function() {
// DEVELOPER, the user has UNmarked this item as done, commence saving!
})
}
});
Color Cycling
We’d better get on this whole “colored” part of Colored Lists eh? CSS will be applying the
actual color, so what we’ll be doing with JavaScript is just cycling the class names applied to
those list items on clicks.
$(".colortab").live("click", function(){
$(this).parent().nextColor();
$.ajax({
// DEVELOPER, the user has toggled the color on this list item, commence saving!
});
});
That nextColor() function isn’t a built-in function, it will be custom written by us. It’s
abstracted away here for code clarity. The way that we’ve used it here (as a part of the
“chain”) is such that we’ll need to make a little jQuery plugin out of it. No problem.
jQuery.fn.nextColor = function() {
if (curColor == "colorBlue") {
$(this).removeClass("colorBlue").addClass("colorYellow").attr("color","2");
} else if (curColor == "colorYellow") {
$(this).removeClass("colorYellow").addClass("colorRed").attr("color","3");
} else if (curColor == "colorRed") {
$(this).removeClass("colorRed").addClass("colorGreen").attr("color","4");
} else {
$(this).removeClass("colorGreen").addClass("colorBlue").attr("color","1");
};
};
Basically this check what color the list item already is and moves it to the next color. Notice
how we are altering an attribute on the list items too. Color isn’t a valid attribute in XHMTL
(it’s fine in HTML5), but oh well. It’s not in the markup so it doesn’t really matter. Why are
we using this? We’ll, it’s because we are about 50% of they way in doing this really
intelligently. When our developer goes to save the color information about this list item to
the database, he needs something to save. Back in Part 2 of this series, we can see that our
developer already anticipated this and created a field for color called listItemColor, which he
made an INT (integer). He figured that would be the smartest way to do it as it’s
lightweight, easy, and abstract. We can decide later what they key is, e.g., 1 = Blue, 2 =
Red, etc. The data itself doesn’t need to know it’s red. So, if we have an integer
representing the color right in the DOM for him, that makes it really easy to snag out and
pass along to save to the database.
Why is this only 50% smart? Well, because we should probably extend that smartness to
the class names themselves. We are using colorYellow for example, when color-1 might
make more sense, if down the line we decide to drop yellow from the lineup and replace it.
Or even perhaps let users declare their own colors.
$(".deletetab").live("click", function(){
// DEVELOPER, the user wants to delete this list item, commence deleting!
success: function(r){
thiscache
.parent()
.hide("explode", 400, function(){$(this).remove()});
});
}
else
{
thiscache.animate({
width: "44px",
right: "-64px"
}, 200)
.data("readyToDelete", "go for it");
}
});
Because we were smart earlier and our little tab graphic is all a part of one sprite graphic,
all we need to do is expand the width of that tab to display the message. After the first
click, we append a little bit of data (jQuery’s data() function) to that list item saying to “go
for it”. Upon a second click, that test will be TRUE and we know we can commence the
deletion of that list item.
Since we are using jQuery UI, we tossed in a little extra fun flair with the “explode” option
for hiding elements.
Back before we had live, we did what we talked briefly earlier. We called a function that did
all our binding. That way we could call it on DOM ready as well as after any AJAX insertions.
We’ll lean on that technique now.
function bindAllTabs(editableTarget) {
$(editableTarget).editable("/path/for/DEVELOPER/to/save.php", {
id : 'listItemID',
indicator : 'Saving...',
tooltip : 'Double-click to edit...',
event : 'dblclick',
submit : 'Save',
submitdata: {action : "update"}
});
}
With those parameters, we’re giving the developer what he needs for a callback file path.
We’re also declaring that we want list items to require a double-click to edit, what the
tooltip will be, and what the text in the saving button will say. Very nicely customizeable!
Remember those spans we inserted around the text of the list items earlier. Now we are
cashing in on that. When we bind this editable function, we’ll bind it to those spans. So right
after DOM ready we’ll call:
bindAllTabs("#list li span");
We’re going to need this function again when appending new list items…
$('#add-new').submit(function(){
if(newListItemText.length > 0) {
$.ajax({
success: function(theResponse){
$("#list").append("<li color='1' class='colorBlue' rel='"+newListItemRel+"' id='" + theResponse +
"'><span id=""+theResponse+"listitem" title='Click to edit...'>" + newListItemText + "</span><div
class='draggertab tab'></div><div class='colortab tab'></div><div class='deletetab tab'></div><div
class='donetab tab'></div></li>");
bindAllTabs("#list li[rel='"+newListItemRel+"'] span");
$("#new-list-item-text").val("");
},
error: function(){
// uh oh, didn't work. Error message?
}
});
} else {
$("#new-list-item-text").val("");
}
return false; // prevent default form submission
});
NOTE: Stay tuned for the final part in the series where will go over some extra security that
needs to happen here. Anytime we accept input from the user, it needs to be scrubbed. This
is no exception.
One last thing we did there was to clear the input field after submission. That makes adding
a bunch of list items in sequence very easy and natural. Also, note that we called that
bindAllTabs function again after the new list item was appended. That’s why we abstracted
that function away to begin with, so we could call it when new list items were appended
AJAX style. That ensures the click-to-edit functionality is working on newly appended list
items even before a page refresh. The rest of the tabs are automatically cool, due to the
live() binding.
Moving On
Up next we’ll pass it back to the developer for filling in those gaps on how those list
interactions work from the PHP / Database side. Then we’ll finish up talking a little security
and wrapping up any loose ends.
Part VII
Where We're At
Now that we've got a set of AJAX controls mostly assembled, we can start building our PHP
class tohandle list interactions. This class will be called ColoredListsItems and it will
reside in the file class.lists.inc.php in the inc folder.
This class will contain methods to handle all of the actions performed by our app or our
users regarding list items. Namely, these actions are:
Also, because we need to be able to load a non-editable version of a list when viewed from
the public URL, we'll need to add that functionality as well.
<?php
/**
* Handles list interactions within the app
*
* PHP version 5
*
* @author Jason Lengstorf
* @author Chris Coyier
* @copyright 2009 Chris Coyier and Jason Lengstorf
* @license http://www.opensource.org/licenses/mit-license.html MI
T License
*
*/
class ColoredListsItems
{
/**
* The database object
*
* @var object
*/
private $_db;
/**
* Checks for a database object and creates one if none is found
*
* @param object $db
* @return void
*/
public function __construct($db=NULL)
{
if(is_object($db))
{
$this->_db = $db;
}
else
{
$dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME;
$this->_db = new PDO($dsn, DB_USER, DB_PASS);
}
}
}
?>
Notice that the constructor is identical to the one we used in ColoredListsUsers (see Part
5—Developing the App: User Interaction ); it checks for a database object and creates one if
none are available.
When our user is logged in, we'll be loading their list by their user name. This user name is
stored in the $_SESSION superglobal .
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Loads all list items associated with a user ID
*
* This function both outputs <li> tags with list items and retur
ns an
* array with the list ID, list URL, and the order number for a n
ew item.
*
* @return array an array containing list ID, list URL, and ne
xt order
*/
public function loadListItemsByUser()
{
$sql = "SELECT
list_items.ListID, ListText, ListItemID, ListItem
Color,
ListItemDone, ListURL
FROM list_items
LEFT JOIN lists
USING (ListID)
WHERE list_items.ListID=(
SELECT lists.ListID
FROM lists
WHERE lists.UserID=(
SELECT users.UserID
FROM users
WHERE users.Username=:user
)
)
ORDER BY ListItemPosition";
if($stmt = $this->_db->prepare($sql))
{
$stmt->bindParam(':user', $_SESSION['Username'], PDO::PAR
AM_STR);
$stmt->execute();
$order = 0;
while($row = $stmt->fetch())
{
$LID = $row['ListID'];
$URL = $row['ListURL'];
echo $this->formatListItems($row, ++$order);
}
$stmt->closeCursor();
// If there aren't any list items saved, no list ID is re
turned
if(!isset($LID))
{
$sql = "SELECT ListID, ListURL
FROM lists
WHERE UserID = (
SELECT UserID
FROM users
WHERE Username=:user
)";
if($stmt = $this->_db->prepare($sql))
{
$stmt->bindParam(':user', $_SESSION['Username'],
PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch();
$LID = $row['ListID'];
$URL = $row['ListURL'];
$stmt->closeCursor();
}
}
}
else
{
echo "\t\t\t\t<li> Something went wrong. ", $db-
>errorInfo, "</li>\n";
}
return array($LID, $URL, $order);
}
}
This method starts with a somewhat complex query that starts
by joining the list_items and lists tables, then uses a sub-query to filter the list
items by user ID.
If the query returns results, we loop through them and call the yet-to-be-defined
formatListItems() method. Each formatted item is output immediately using echo() ,
and the list ID and URL are saved.
If no results are returned (meaning the user doesn't have any items on their list), the list ID and
URL won't be returned. However, we need this information in order to allow users to submit new
items and share their lists. A check is in place to see if the list ID variable ( $LID ) is set. If not,
an additional query is executed to retrieve the user's list ID and URL.
If no user is logged in, we don't have access to a user name with which to load the
items. Therefore,we need to use the list's ID to load items. We're able to determine the list's
ID using the list's URL, which is the only way a user who isn't logged in will be able to view a list
in the first place. The method looks like this in inc/class.lists.inc.php :
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Outputs all list items corresponding to a particular list ID
*
* @return void
*/
public function loadListItemsByListId()
{
$sql = "SELECT ListText, ListItemID, ListItemColor, ListItemD
one
FROM list_items
WHERE ListID=(
SELECT ListID
FROM lists
WHERE ListURL=:list
)
ORDER BY ListItemPosition";
if($stmt = $this->_db->prepare($sql)) {
$stmt->bindParam(':list', $_GET['list'], PDO::PARAM_STR);
$stmt->execute();
$order = 1;
while($row = $stmt->fetch())
{
echo $this->formatListItems($row, $order);
++$order;
}
$stmt->closeCursor();
} else {
echo "<li> Something went wrong. ", $db->error, "</li>";
}
}
}
To make our previous two methods work properly, we also need to define our
formatListItems() method. This method is fairly straightforward, but it also needs a
helper method to determine the CSS class for each item, which we'll call getColorClass() .
This helper method only exists to simplify our code. Insert both methods
into inc/class.lists.inc.php as follows:
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Generates HTML markup for each list item
*
* @param array $row an array of the current item's attributes
* @param int $order the position of the current list item
* @return string the formatted HTML string
*/
private function formatListItems($row, $order)
{
$c = $this->getColorClass($row['ListItemColor']);
if($row['ListItemDone']==1)
{
$d = '<img class="crossout" src="/images/crossout.png" '
. 'style="width: 100%; display: block;"/>';
}
else
{
$d = NULL;
}
// If not logged in, manually append the <span> tag to each i
tem
if(!isset($_SESSION['LoggedIn'])||$_SESSION['LoggedIn']!=1)
{
$ss = "<span>";
$se = "</span>";
}
else
{
$ss = NULL;
$se = NULL;
}
return "\t\t\t\t<li id=\"$row[ListItemID]\" rel=\"$order\" "
. "class=\"$c\" color=\"$row[ListItemColor]\">$ss"
. htmlentities(strip_tags($row['ListText'])).$d
. "$se</li>\n";
}
/**
* Returns the CSS class that determines color for the list item
*
* @param int $color the color code of an item
* @return string the corresponding CSS class for the color
code
*/
private function getColorClass($color)
{
switch($color)
{
case 1:
return 'colorBlue';
case 2:
return 'colorYellow';
case 3:
return 'colorRed';
default:
return 'colorGreen';
}
}
}
An array containing each list item's attributes is passed to formatListItems() ,
and different attributes are created depending on the values that are passed. If a user isn't
logged in, we manually append a <span> to the markup to keep our CSS from breaking, and
then we wrap the whole thing in a <li> and return it.
Our main page ( index.php ) currently has notes from the designer that look like this:
<div id="main">
<noscript>This site just doesn't work, period, without JavaScript<
/noscript>
<!-- IF LOGGED IN -->
<!-- Content here -->
<!-- IF LOGGED OUT -->
<!-- Alternate content here -->
</div>
In order to make these notes into a functional script, we need to add the following logic
to index.php to call the proper methods:
<div id="main">
<noscript>This site just doesn't work, period, without Ja
vaScript</noscript>
<?php
if(isset($_SESSION['LoggedIn']) && isset($_SESSION['Username'])):
echo "\t\t\t<ul id=\"list\">\n";
include_once 'inc/class.lists.inc.php';
$lists = new ColoredListsItems($db);
list($LID, $URL, $order) = $lists->loadListItemsByUser();
echo "\t\t\t</ul>";
?>
<br />
<form action="db-interaction/lists.php" id="add-new" meth
od="post">
<input type="text" id="new-list-item-text" name="new-
list-item-text" />
<input type="hidden" id="current-list" name="current-
list" value="<?php echo $LID; ?>" />
<input type="hidden" id="new-list-item-position" nam
e="new-list-item-position" value="<?php echo ++$order; ?>" />
<input type="submit" id="add-new-submit" value="Add"
class="button" />
</form>
<div class="clear"></div>
<div id="share-area">
<p>Public list URL: <a target="_blank" href="http://c
oloredlists.com/<?php echo $URL ?>.html">http://coloredlists.com/<?
php echo $URL ?>.html</a>
<small>(Nobody but YOU will be able to edit th
is list)</small></p>
</div>
<script type="text/javascript" src="js/jquery-ui-
1.7.2.custom.min.js"></script>
<script type="text/javascript" src="js/jquery.jeditable.m
ini.js"></script>
<script type="text/javascript" src="js/lists.js"></script
>
<script type="text/javascript">
initialize();
</script>
<?php
elseif(isset($_GET['list'])):
echo "\t\t\t<ul id='list'>\n";
include_once 'inc/class.lists.inc.php';
$lists = new ColoredListsItems($db);
list($LID, $URL) = $lists->loadListItemsByListId();
echo "\t\t\t</ul>";
else:
?>
<img src="/images/newlist.jpg" alt="Your new list here!"
/>
<?php endif; ?>
</div>
This script checks if a user is logged in, then outputs their list and the proper controls if
so. If not, we check if there was a list URL supplied and outputs a non-editable list if one is
found. Otherwise, the "sales" page is displayed, encouraging the viewer to sign up.
The PHP
Saving an item is simple enough on the server side. We simply grab all of the new item's
information out of the $_POST superglobal, prepare a statement, and save the info in the
database. Note that we're running strip_tags() on the list item's text. This is a redundant
check since we're using JavaScript to remove any unwanted tags, but we shouldn't rely on
data that was sanitized client-side.
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Adds a list item to the database
*
* @return mixed ID of the new item on success, error message
on failure
*/
public function addListItem()
{
$list = $_POST['list'];
$text = strip_tags(urldecode(trim($_POST['text'])), WHITELIST
);
$pos = $_POST['pos'];
$sql = "INSERT INTO list_items
(ListID, ListText, ListItemPosition, ListItemColo
r)
VALUES (:list, :text, :pos, 1)";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':list', $list, PDO::PARAM_INT);
$stmt->bindParam(':text', $text, PDO::PARAM_STR);
$stmt->bindParam(':pos', $pos, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return $this->_db->lastInsertId();
}
catch(PDOException $e)
{
return $e->getMessage();
}
}
}
Notice that we used a constant called WHITELIST in the strip_tags() function. This is a
list of allowed tags that our users have access to. However, we should assume that we'll want
to change this list at some point in the future, which is why we're saving the list as a constant,
which we'll define in inc/constants.inc.php :
// HTML Whitelist
define('WHITELIST', '<b><i><strong><em><a>');
Finishing the JavaScript
To complete the jQuery in js/lists.js , we need to modify the script with the code below:
// AJAX style adding of list items
$('#add-new').submit(function(){
// HTML tag whitelist. All other tags are stripped.
var $whitelist = '<b><i><strong><em><a>',
forList = $("#current-list").val(),
newListItemText = strip_tags(cleanHREF($("#new-list-item-
text").val()), $whitelist),
URLtext = escape(newListItemText),
newListItemRel = $('#list li').size()+1;
if(newListItemText.length > 0) {
$.ajax({
type: "POST",
url: "db-interaction/lists.php",
data: "action=add&list=" + forList + "&text=" + URLte
xt + "&pos=" + newListItemRel,
success: function(theResponse){
$("#list").append("<li color='1' class='colorBlue'
rel='"+newListItemRel+"' id='" + theResponse + "'><span id=\""+theRes
ponse+"listitem\" title='Click to edit...'>" + newListItemText + "</s
pan><div class='draggertab tab'></div><div class='colortab tab'></div
><div class='deletetab tab'></div><div class='donetab tab'></div></li
>");
bindAllTabs("#list li[rel='"+newListItemRel+"'] spa
n");
$("#new-list-item-text").val("");
},
error: function(){
// uh oh, didn't work. Error message?
}
});
} else {
$("#new-list-item-text").val("");
}
return false; // prevent default form submission
});
We're completing the $.ajax() call by submitting the new item via the POST method to db-
interation/lists.php . The successfully added item is then appended to our list, all
without a page refresh.
Our $.ajax() call sends to db-interaction/lists.php , which doesn't exist yet. This
script acts as a switch that will determine what action is needed and execute the proper
method. All requests are handled the same way, so let's just define the whole file here. Create
new file called lists.php in the db-interaction folder and insert the following code into
it:
<?php
session_start();
include_once "../inc/constants.inc.php";
include_once "../inc/class.lists.inc.php";
if(!empty($_POST['action'])
&& isset($_SESSION['LoggedIn'])
&& $_SESSION['LoggedIn']==1)
{
$listObj = new ColoredListsItems();
switch($_POST['action'])
{
case 'add':
echo $listObj->addListItem();
break;
case 'update':
$listObj->updateListItem();
break;
case 'sort':
$listObj->changeListItemPosition();
break;
case 'color':
echo $listObj->changeListItemColor();
break;
case 'done':
echo $listObj->toggleListItemDone();
break;
case 'delete':
echo $listObj->deleteListItem();
break;
default:
header("Location: /");
break;
}
}
else
{
header("Location: /");
exit;
}
?>
The PHP
Each item is assigned a position when it's read out of the database. This is the
item's starting position. When it is dragged, it ends up in a new place in the list; we're going to
call this new position it's current position.
When changeListItemPosition() is called, both the item's starting position and current
position are passed, as well as the direction it moved. This is where it gets tricky.
Depending on the direction the item was moved, we set up one of two conditional queries.
We select all the items in the current list with a position falling between the starting and
current positions , then, using the CASE clause , increment or decrement their positions
by 1 unless the item's position plus or minus one falls outside the range we've selected, at
which point we set the item's position to the current position. In this way, we're able to
avoid firing an individual query for each item, which could potentially cause a performance
bottleneck.
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Changes the order of a list's items
*
* @return string a message indicating the number of affected
items
*/
public function changeListItemPosition()
{
$listid = (int) $_POST['currentListID'];
$startPos = (int) $_POST['startPos'];
$currentPos = (int) $_POST['currentPos'];
$direction = $_POST['direction'];
if($direction == 'up')
{
/*
* This query modifies all items with a position between
the item's
* original position and the position it was moved to. If
the
* change makes the item's position greater than the item
's
* starting position, then the query sets its position to
the new
* position. Otherwise, the position is simply incremente
d.
*/
$sql = "UPDATE list_items
SET ListItemPosition=(
CASE
WHEN ListItemPosition+1>$startPos THEN $c
urrentPos
ELSE ListItemPosition+1
END)
WHERE ListID=$listid
AND ListItemPosition BETWEEN $currentPos AND $sta
rtPos";
}
else
{
/*
* Same as above, except item positions are decremented,
and if the
* item's changed position is less than the starting posi
tion, its
* position is set to the new position.
*/
$sql = "UPDATE list_items
SET ListItemPosition=(
CASE
WHEN ListItemPosition-1<$startPos THEN $c
urrentPos
ELSE ListItemPosition-1
END)
WHERE ListID=$listid
AND ListItemPosition BETWEEN $startPos AND $curre
ntPos";
}
$rows = $this->_db->exec($sql);
echo "Query executed successfully. ",
"Affected rows: $rows";
}
}
Finishing the JavaScript
To call our method, we need to modify js/lists.js by adding a new function
called saveListOrder() :
function saveListOrder(itemID, itemREL){
var i = 1,
currentListID = $('#current-list').val();
$('#list li').each(function() {
if($(this).attr('id') == itemID) {
var startPos = itemREL,
currentPos = i;
if(startPos < currentPos) {
var direction = 'down';
} else {
var direction = 'up';
}
var postURL = "action=sort¤tListID="+currentListID
+"&startPos="+startPos
+"¤tPos="+currentPos
+"&direction="+direction;
$.ajax({
type: "POST",
url: "db-interaction/lists.php",
data: postURL,
success: function(msg) {
// Resets the rel attribute to reflect current po
sitions
var count=1;
$('#list li').each(function() {
$(this).attr('rel', count);
count++;
});
},
error: function(msg) {
// error handling here
}
});
}
i++;
});
}
This function accepts the ID and rel attribute of the item that was moved. The rel attribute
contains the original position of the item, which we need as its starting position. Then we loop
through each list item while incrementing a counter ( i ). When we find the list item that
matches the moved item's ID, our counter now reflects the item's current position. We can then
determine which direction the item was moved and send the info to db-
interaction/lists.php for processing.
This function needs to be called when a sortable item is updated, which we accomplish by
modifying the following in js/lists.js :
// MAKE THE LIST SORTABLE VIA JQUERY UI
// calls the SaveListOrder function after a change
// waits for one second first, for the DOM to set, otherwise it's
too fast.
$("#list").sortable({
handle : ".draggertab",
update : function(event, ui){
var id = ui.item.attr('id');
var rel = ui.item.attr('rel');
var t = setTimeout("saveListOrder('"+id+"', '"+rel+"')",5
00);
},
forcePlaceholderSize: true
});
The PHP
To update an item's color, we simply pass it's ID and the new color code to the method
changeListItemColor() and create and execute a query.
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Changes the color code of a list item
*
* @return mixed returns TRUE on success, error message on fai
lure
*/
public function changeListItemColor()
{
$sql = "UPDATE list_items
SET ListItemColor=:color
WHERE ListItemID=:item
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':color', $_POST['color'], PDO::PARAM_IN
T);
$stmt->bindParam(':item', $_POST['id'], PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return TRUE;
} catch(PDOException $e) {
return $e->getMessage();
}
}
}
Finishing the JavaScript
The function that saves new colors is called by submitting the item ID and new color via POST in
the $.ajax() call below in js/lists.js :
// COLOR CYCLING
// Does AJAX save, but no visual feedback
$(".colortab").live("click", function(){
$(this).parent().nextColor();
var id = $(this).parent().attr("id"),
color = $(this).parent().attr("color");
$.ajax({
type: "POST",
url: "db-interaction/lists.php",
data: "action=color&id=" + id + "&color=" + color,
success: function(msg) {
// error message
}
});
});
The PHP
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Updates the text for a list item
*
* @return string Sanitized saved text on success, error messa
ge on fail
*/
public function updateListItem()
{
$listItemID = $_POST["listItemID"];
$newValue = strip_tags(urldecode(trim($_POST["value"])), WHIT
ELIST);
$sql = "UPDATE list_items
SET ListText=:text
WHERE ListItemID=:id
LIMIT 1";
if($stmt = $this->_db->prepare($sql)) {
$stmt->bindParam(':text', $newValue, PDO::PARAM_STR);
$stmt->bindParam(':id', $listItemID, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
echo $newValue;
} else {
echo "Error saving, sorry about that!";
}
}
}
Finishing the JavaScript
// This is seperated to a function so that it can be called at page l
oad
// as well as when new list items are appended via AJAX
function bindAllTabs(editableTarget) {
// CLICK-TO-EDIT on list items
$(editableTarget).editable("db-interaction/lists.php", {
id : 'listItemID',
indicator : 'Saving...',
tooltip : 'Double-click to edit...',
event : 'dblclick',
submit : 'Save',
submitdata: {action : "update"}
});
}
The PHP
The toggleListItemDone() method retrieves the item's ID and "done" status from the
$_POST superglobal and uses them to update the item in the database:
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Changes the ListItemDone state of an item
*
* @return mixed returns TRUE on success, error message on fai
lure
*/
public function toggleListItemDone()
{
$sql = "UPDATE list_items
SET ListItemDone=:done
WHERE ListItemID=:item
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':done', $_POST['done'], PDO::PARAM_INT)
;
$stmt->bindParam(':item', $_POST['id'], PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return TRUE;
} catch(PDOException $e) {
return $e->getMessage();
}
}
}
Finishing the JavaScript
To call our method, we write a function called toggleDone() in js/lists.js . This
function simply executes a call to the $.ajax() function and sends the item ID and "done"
status to our list handler.
function toggleDone(id, isDone)
{
$.ajax({
type: "POST",
url: "db-interaction/lists.php",
data: "action=done&id="+id+"&done="+isDone
})
}
Next, we assign toggleDone() as the callback function for the animate() even that
happens when our user clicks the done tab:
$(".donetab").live("click", function() {
var id = $(this).parent().attr('id');
if(!$(this).siblings('span').children('img.crossout').length)
{
$(this)
.parent()
.find("span")
.append("<img src='/images/crossout.png' class='c
rossout' />")
.find(".crossout")
.animate({
width: "100%"
})
.end()
.animate({
opacity: "0.5"
},
"slow",
"swing",
toggleDone(id, 1));
}
else
{
$(this)
.siblings('span')
.find('img.crossout')
.remove()
.end()
.animate({
opacity : 1
},
"slow",
"swing",
toggleDone(id, 0));
}
});
Deleting Items
Finally, we need to allow our users to delete items that they no longer want on their list.
The PHP
class ColoredListsItems
{
// Class properties and other methods omitted to save space
/**
* Removes a list item from the database
*
* @return string message indicating success or failure
*/
public function deleteListItem()
{
$list = $_POST['list'];
$item = $_POST['id'];
$sql = "DELETE FROM list_items
WHERE ListItemID=:item
AND ListID=:list
LIMIT 1";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':item', $item, PDO::PARAM_INT);
$stmt->bindParam(':list', $list, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
$sql = "UPDATE list_items
SET ListItemPosition=ListItemPosition-1
WHERE ListID=:list
AND ListItemPosition>:pos";
try
{
$stmt = $this->_db->prepare($sql);
$stmt->bindParam(':list', $list, PDO::PARAM_INT);
$stmt->bindParam(':pos', $_POST['pos'], PDO::PARAM_IN
T);
$stmt->execute();
$stmt->closeCursor();
return "Success!";
}
catch(PDOException $e)
{
return $e->getMessage();
}
}
catch(Exception $e)
{
return $e->getMessage();
}
}
}
Finishing the JavaScript
To activate this method, we need to modify our jQuery by updating the section
in js/lists.js that deals with item deletion:
// AJAX style deletion of list items
$(".deletetab").live("click", function(){
var thiscache = $(this),
list = $('#current-list').val(),
id = thiscache.parent().attr("id"),
pos = thiscache.parents('li').attr('rel');
if (thiscache.data("readyToDelete") == "go for it") {
$.ajax({
type: "POST",
url: "db-interaction/lists.php",
data: {
"list":list,
"id":id,
"action":"delete",
"pos":pos
},
success: function(r){
var $li = $('#list').children('li'),
position = 0;
thiscache
.parent()
.hide("explode", 400, function(){$
(this).remove()});
$('#list')
.children('li')
.not(thiscache.parent())
.each(function(){
$(this).attr('rel', +
+position);
});
},
error: function() {
$("#main").prepend("Deleting the item failed...
");
}
});
}
else
{
thiscache.animate({
width: "44px",
right: "-64px"
}, 200)
.data("readyToDelete", "go for it");
}
});
Moving On
We have now succeeded in building all of the AJAX functionality for our app! There was a ton of
information in this article, and we went through it rather quickly, so please post any questions
you have in the comments!
In the final installment of this series, we'll go over the security measures and other finishing
touches this app needs to be ready for public use. We'll also go over some of the features we
hope to add in the future.
Part VIII
Hooray we made it! First of all, thanks for following along this whole journey. You can go
check out the real live app for yourselves:
http://coloredlists.com
Below we’re going to wrap up a few things by talking about some of the choices we made,
security precautions, and ideas we (and you) have for a version 2.0 of this app.
Object-Oriented Programming
Because we should always aim to be efficient when programming, we built this app with the
concept of DRY programming in mind. DRY stands for “Don’t Repeat Yourself” and should lie
somewhere near the core of our programming philosophy.
In our opinion, taking the object-oriented programming (OOP) approach was the best way
to keep this app DRY. OOP allows us to group common methods together and separate
tasks out without needing to pass parameters from function to function. For a little more
information on OOP and why it’s beneficial, read Jason’s introduction to OOP.
Security
Security is incredibly important in any application. We have users with accounts who are
storing data with us. Those users are putting their trust in us to make sure their data is
safe, which includes their password and all the information they’ve entered into the lists.
This app is already pretty darn secure. Passwords are stored in encrypted formats and
never sent in the clear via Email. All the interaction that happens with the database is
secure. Only users who are logged in can issue commands which result in database
changes, and those users are only able to issue commands that affect their own data.
But because there is a variety of AJAX stuff going on in this app, our security needs to take
into account a few more scenarios. First, our JavaScript (like all JavaScript) is publically
viewable. This JavaScript contains the code for making AJAX calls, meaning the URL we are
sending to and what data that URL is expecting. This tells potential attackers a good bit of
information regarding how they might send malicious requests. Because of this, we need to
be very careful and ensure that all incoming data is escaped properly.
PDO
Database attacks, called SQL injection, are a particularly nasty form of attack. A vulnerable
database can be read, manipulated, or deleted entirely by a malicious user. This means that
it is really important that we keep any kind of SQL injection from happening.
Lucky for us, PHP Data Objects (PDO) virtually eliminates the risk for SQL injection through
the use of prepared statements, which are like query templates that we can customize with
parameters. All the escaping is done for us when the parameters are inserted into the
query, so it’s virtually impossible for SQL injection to occur while using prepared
statements.
It was because of this powerful security advantage that we chose PDO for this app. (Keep in
mind that prepared statements are not exclusive to PDO; other database extensions, such
as MySQLi, also support them.)
Data Escaping
While PDO is powerful against SQL injection, it doesn’t help us when we’ve read the
information out of the database. If a malicious user injects dangerous tags into our
database, they’ll still be dangerous when they’re retrieved unless we take further measures
to sanitize user data.
Fortunately, PHP has built-in functions that will allow us to perform basic sanitization of user
input. We’re namely using strip_tags() with a whitelist to make sure no <script> tags or
other potentially dangerous tags make it into the database. Also, because we never want
that sort of thing to be allowed, we’re performing this escaping before the data is inserted
into the database.
Secondly, because we are inputting data and turn it around to display immediately on the
screen, it’s best to do some of that input scrubbing directly in the JavaScript. When a user
enters a new list item, we’ll take two steps to scrub it. First we’ll ensure they aren’t
naughtily trying to insert immediately executable JavaScript into links:
NOTE: The strip_tags() function used below is part of the php.js project, which has ported a
number of useful PHP functions to JavaScript.
var $whitelist = '<b><i><strong><em><a>',
str += '';
// Match tags
matches = str.match(/(<\/?[\S][^>]*>)/gi);
if (i != 0) { i = html.toLowerCase().indexOf('<'+allowed_tag+'>');}
if (i != 0) { i = html.toLowerCase().indexOf('<'+allowed_tag+' ');}
if (i != 0) { i = html.toLowerCase().indexOf('</'+allowed_tag) ;}
// Determine
if (i == 0) {
allowed = true;
break;
}
}
if (!allowed) {
str = replacer(html, "", str); // Custom replace. No regexing
}
}
return str;
}
These functions are implemented in js/lists.js before sending off the AJAX request that adds
a new list item…
...
// AJAX style adding of list items
$('#add-new').submit(function(){
// HTML tag whitelist. All other tags are stripped.
var $whitelist = '<b><i><strong><em><a>',
forList = $("#current-list").val();
newListItemText = strip_tags(cleanHREF($("#new-list-item-text").val()), $whitelist),
...
POST vs GET
One last small measure we’ve taken to secure our app is to use POST over GET for all of our
AJAX calls. This is done because the GET method should only be used for retrieval, and not
for any action that will modify data in any way.
The primary reason not to use GET for modifying data is that a request made using GET is
sent in the URL (i.e. http://example.com?get=request&is=this&part=here). There’s an
inherent danger in modifying data based on the information passed in the URL in that a user
can cause duplicate processing by accidentally refreshing his or her browser.
A secondary, less important reason to use POST is that it’s a little harder to send a bogus
request using POST, which provides a (minor) deterrent to malicious users.
2.0 Features
Of course our work as designers and developers is never done. This is a great start on a
simple and usable list application, but right away new features jump to mind. Here are some
ideas of ways to expand functionality. Perhaps they slightly complicate things, but are all
probably great ideas assuming they are implemented well.
List sharing
Enter an email address for someone to share the list with. Sharing meaning literally
collaborative editing. The user would need an account, so if they already have one they
would just be emailed and asked to join the list (they can accept or not accept). If that
email address did not have an account, they would be promoted to join first.
Multiple lists
Right now a user can have only one list. It would probably be useful for users to keep
multiple lists. Perhaps a dropdown menu for toggling between lists and a simple button
for adding new ones. Plenty of interface to think about here, including figuring out how
to delete lists.
RSS
Each list could have it’s own RSS feed. Options would probably be necessary, like what
the RSS feed would contain (e.g. Do you wish to see entries for when list items are
completed or not?). Feed URLs could be long gibberish URL’s, so they are essentially
completely private unless specifically shared.
iPhone interface
Logging in via iPhone or other mobile device would have a better more optimized
experience.