LanguagesPHPLearning WML -Scripting Tips And Integration With PHP

Learning WML -Scripting Tips And Integration With PHP

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Project Description

In our last article we implemented a simple phone list using PHP and MySQL. The basic premise was to give salespeople access to a central contact database without the need of synchronizing their phones/PDAs. The script pulled contact data from a database, displayed the records, and dialed the selected record.
This project will expand on that example, adding the following features:

  • The contact database will include address information.
  • There will be more contacts, requiring the list to be
    displayed a few records at a time.

  • A search feature will be added.

Because there will be more than one feature (list and search), we will also need a menu so the user can choose what feature he or she wants. Each screen will also need a way to return to the menu.

PHP Script Outline

The project will use one PHP script and a controlling variable ($cmd) that tells the script card that it is supposed to display. The script recursively calls itself with the proper value of $cmd, matching the function it is supposed to perform next. The script’s functionality resembles the following:

   If $cmd is empty, set it to "Menu"

   If $cmd is set to "Menu," display the Menu card
   If $cmd is set to "List," display the List card
   If $cmd is set to "Display," 
         display the Display card, with the appropriate record
   If $cmd is set to "Search," display the Search card
   
   When a function is chosen, 
        set $cmd appropriately and recursively call self.

The search function will search through the database looking for first or last names that meet the text entered in the search card. The list function will display five records at a time and will include options for moving to the next set of five records or returning to the home menu. Each option (record or link) will have a quick key. Essentially, the list will resemble the following:

  1. <DB record>
  2. <DB record>
  3. <DB record>
  4. <DB record>
  5. <DB record>
  6. [Next 5 link]
  7. [Home link]

Selecting a record results in displaying all of the data associated with the record, along with “dial” and “home” links.

The Contact List

Our contact database will contain the following data:

 



































































































































































First
Name


Last
Name


Phone


Address


City


State


Zip


Last
Update


Jack


Hampton


317-555-2200


213 Main St


Indianapolis


IN


46222


2002-08-22


Samuel


Marks


317-555-8764


2 Northridge Dr


Fishers


IN


46203


2002-08-22


Sally


Nash


317-555-8765


644 Innovation Pl


Ft Wayne


IN


46875


2002-08-22


Bill


Haskins


317-555-8766


201 W 103rd


Indianapolis


IN


46240


2002-08-24


Jill


Payton


317-555-0098


55 W 96th St


Westfield


IN


46222


2002-08-22


Mary


Martinez


317-555-7544


9433 E. 75th Ave


Greenwood


IN


46784


2002-08-22


Ned


Tanner


317-555-9877


77 E Marchen


Ft Wayne


IN


46875


2002-08-23


Bruce


Wilten


317-555-1111


3 Prospect


Indianapolis


IN


46038


2002-08-22


Naomi


Waters


317-555-4323


1121 Central Pl


Westfield


IN


46055


2002-08-22


Angela


Renault


317-555-0988


5674 E 6th Ave


Noblesville


IN


46234


2002-08-22


Markus


Elliot


317-555-3232


9755 Carter


Indianapolis


IN


46250


2002-08-22


Steve


Albert


317-555-5444


95 Crescent Dr


Indianapolis


IN


46250


2002-08-26


Martin


Rolfsen


317-555-6767


5678 E 7th Ave


Indianapolis


IN


46234


2002-08-22


Lisa


Biggins


317-555-3644


7732 Allisonville


Indianapolis


IN


46240


2002-08-22


Eric


Gonday


317-555-0500


9466 Pike Plaza


Greenfield


IN


46533


2002-08-22


Douglas


Poser


317-555-0123


55 Tower Pl


Noblesville


IN


46234


2002-08-24


John


Palmer


317-555-4444


12433 N Cumberland


Fishers


IN


46038


2002-08-22

The database will also contain an “ID” field as a primary key (integer). That will serve as a unique key into each record. To create the data in MySQL, the following commands should be used:

CREATE DATABASE customers;

USE customers;

CREATE TABLE Phone (
  Id int(11) NOT NULL auto_increment,
  FirstName varchar(30) default NULL,
  LastName varchar(30) default NULL,
  Phone varchar(12) default NULL,
  Address varchar(30) default NULL,
  City varchar(30) default NULL,
  State char(2) default NULL,
  Zip varchar(5) default NULL,
  LastUpdate date default NULL,
  PRIMARY KEY  (Id)
) TYPE=MyISAM;

INSERT INTO Phone VALUES (1,'Jack','Hampton','317-555-2200',
'213 Main St','Indianapolis','IN','46222','2002-08-22');
INSERT INTO Phone VALUES (2,'Samuel','Marks','317-555-8764',
'2 Northridge Dr','Fishers','IN','46203','2002-08-22');
INSERT INTO Phone VALUES (3,'Sally','Nash','317-555-8765',
'644 Innovation Pl','Ft Wayne','IN','46875','2002-08-22');
INSERT INTO Phone VALUES (4,'Bill','Haskins','317-555-8766',
'201 W 103rd','Indianapolis','IN','46240','2002-08-24');
INSERT INTO Phone VALUES (5,'Jill','Payton','317-555-0098',
'55 W 96th St','Westfield','IN','46222','2002-08-22');
INSERT INTO Phone VALUES (6,'Mary','Martinez','317-555-7544',
'9433 E 75th Ave','Greenwood','IN','46784','2002-08-22');
INSERT INTO Phone VALUES (7,'Ned','Tanner','317-555-9877',
'77 E Marchen','Ft Wayne','IN','46875','2002-08-23');
INSERT INTO Phone VALUES (8,'Bruce','Wilten','317-555-1111',
'3 Prospect','Indianapolis','IN','46038','2002-08-22');
INSERT INTO Phone VALUES (9,'Naomi','Waters','317-555-4323',
'1121 Central Pl','Westfield','IN','46055','2002-08-22');
INSERT INTO Phone VALUES (10,'Angela','Renault','317-555-0988',
'5674 E 6th Ave','Noblesville','IN','46234','2002-08-22');
INSERT INTO Phone VALUES (11,'Markus','Elliot','317-555-3232',
'9755 Carter','Indianapolis','IN','46250','2002-08-22');
INSERT INTO Phone VALUES (12,'Steve','Albert','317-555-5444',
'95 Crescent Dr','Indianapolis','IN','46250','2002-08-26');
INSERT INTO Phone VALUES (13,'Martin','Rolfsen','317-555-6767',
'5678 E 7th Ave','Indianapolis','IN','46234','2002-08-22');
INSERT INTO Phone VALUES (14,'Lisa','Biggins','317-555-3644',
'7732 Allisonville','Indianapolis','IN','46240','2002-08-22');
INSERT INTO Phone VALUES (15,'Eric','Gonday','317-555-0500',
'9466 Pike Plaza','Greenfield','IN','46533','2002-08-22');
INSERT INTO Phone VALUES (16,'Douglas','Poser','317-555-0123',
'55 Tower Pl','Noblesville','IN','46234','2002-08-24');
INSERT INTO Phone VALUES (17,'John','Palmer','317-555-4444',
'12433 N Cumberland','Fishers','IN','46038','2002-08-22');

The Cards

The following sections list the PHP code used for each function/card. Note that the WML and header code is taken care of by global statements-each card need only output the <card> tags and everything in-between. The $cmd variable controls what function is executed, and hence, which card is displayed.

Note: I’ve used “echo” commands in this script, but “print” commands would do just as well. Also, to increase the readability of the output I’ve added line feeds (via a variable, $lf, set to ASCII character 10) to most lines of output. I’ve chosen to append this variable to the “echo” statements for readability purposes–using “n” in your “echo” commands would do just as well but tends to clutter the information between the quotes. Also note the use of the entity “&amp;” in the URLs instead of a straight ampersand (“&”). This is necessary to keep WML from assuming that the ampersand is the beginning of an entity name.

Menu

The menu function displays a simple select list, allowing the user to choose what function he or she wants to access:

  echo "<card id="Menu">n";
  echo "<p mode="nowrap">".$lf;
  
  // Set up Select menu list 
  echo "<select name="Select" title="Select:">".$lf;

  // Go through results from Query, listing each as a CHOICE entry
  echo "<option onpick="?cmd=List">";
  echo "List Contacts</option>".$lf;
  echo "<option onpick="?cmd=Search">";
  echo "Search Contacts</option>".$lf;
  
  // Close select
  echo "</select>".$lf;

  // Close card
  echo "</p>".$lf."</card>".$lf;

The code is straightforward, defining a simple <select> list. Each <option> in the list calls the current script, passing the appropriate value of $cmd. Note that we need to handle the case when $cmd is empty, which it will be when the script is first called. In the body, near the beginning of the script, we add the following line:

  if (empty($cmd)) { $cmd = "Menu"; }

That ensures that if no command is given (via $cmd), the menu will be displayed.

List

This is the meat of the script, displaying both the raw list as well as search results, five records at a time. (For reference after the listing, each line has been numbered.)

1  // Construct appropriate *count* query
2:   $query = "select count(*) from Phone";
3:   if (!empty($search)) {
4:     $query = $query." where FirstName like "%".$search."%" or";
5:     $query = $query." LastName like "%".$search."%"";
6:   }

7:   $result = mysql_query($query,$link)
8:     or die("Query failed:$query");

9:   list($total_rows) = mysql_fetch_array($result);

10:   // Construct appropriate query
11:   $query = "select * from Phone";
12:   if (!empty($search)) {
13:     $query = $query." where FirstName like "%".$search."%" or";
14:     $query = $query." LastName like "%".$search."%"";
15:   }

16:   // Get first/next five records
17:   $query = $query." order by LastName limit ".$idx.",5";

18:   $result = mysql_query($query,$link)
19:     or die("Query failed:$query");

20:   // Advance DB index
21:   $next = $idx + 5;

22:   // Start card
23:   echo "<card id="Contacts">n";
24:   echo "<do type="accept" label="View"> <go href=""/> </do>".$lf;

25:   // Display appropriate full/search heading
26:   if (empty($search)) {
27:     echo "<p mode="nowrap"><b>Phone Book</b>".$lf;
28:   } else {
29:     echo "<p mode="nowrap"><b>Search Results</b>".$lf;
30:   }

31:   // Set up Select list (list of five records)
32:   echo "<select name="View" title="View:">".$lf;

33:   // Go through results from Query, listing each as a CHOICE entry
34:   while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {

35:     $recordid = $line[Id]; 
36:     $Name = $line[LastName] . ", " . $line[FirstName];
37:     $Number = $line[Phone];
38:     $Prompt = $Name . " (" . $Number . ")";

39:   // Build URL for option, include DB index and search
40:     $option = "<option onpick="";
41:     $option = $option."?cmd=Display&amp;id=".$recordid."&amp";
42:     $option =
   $option.";idx=".$recordid."&amp;search=".$search."">";
43:     $option = $option.$Prompt."</option>".$lf;
44:     echo $option.$lf;

45:   }

46:   // If there are more records to display, set up paging
47:   //   else mark end of list (to keep Home as same option)
48:   if ($total_rows >= $next) {

49:   // Link to next five
50:     echo "<option title="Next" onpick="?cmd=List&amp;idx=$next";

51:   // Pass Search criteria if exists
52:     if (!empty($search)) {
53:       echo "&amp;search=".$search;
54:     }

55:     echo "">[Next Records]".$lf."</option>".$lf;

56:   } else {

57:     echo "<option title="End of List" onpick="?cmd=List&amp;idx=$idx";

58:   // Pass Search criteria if exists
59:     if (!empty($search)) {
60:     echo "&amp;search=".$search;
61:     }

62:   // Close tags
63:     echo "">[End of List]".$lf."</option>".$lf;

64:   }          

65:   // Add option for Home
66:   echo "<option onpick="?cmd=Menu" title="Home">".$lf;
67:   echo "[Back to Home]".$lf;
68:   echo "</option>".$lf;

69:   // Close select
70:   echo "</select>".$lf;

71:   // Close card
72:   echo "</p>".$lf."</card>".$lf;

This code makes use of global variables ($idx, $search) to display the list of contacts. Both variables are passed as a name/value pair when the script is called with $cmd equal to “List.” If $search is empty, the full list of contacts is displayed, else the text of $search is added to the query and records are returned only if FirstName or LastName contains the search text.

The $idx variable marks what results the script currently is listing. The record at location $idx is the first record on the current page.

This becomes more self-explanatory as we work through the code:

Lines 1-9 construct and execute a “count” query, storing the number of returned rows in the variable $total_rows. This value is used later (line 48) to determine whether there are more pages of data to display. Note that the search text is added if $search is not empty (hence contains search criteria).

Lines 10-19 construct a query to return the target dataset. Again, the search criterion is added to the query, if it exists. Line 17 appends a limit clause to the query, causing the query to return only five records (or less), starting at the record indicated by the value of $idx. If $idx is zero (which it will be the first time the script executes List), the first five records are returned. The variable $next is set to a value of $idx + 5 (line 21) and used to call the next iteration of List, causing the next five records to be displayed.

Note: This method is far from perfect. For example, if a record is added or modified that causes a record to be added or removed from the returned dataset, the next page will return different results than it would before the record was added or modified. If the list is being displayed while the records are being modified-which happens in most database applications-the displayed results can be somewhat unpredictable.

Lines 22-30 begin the display card definition, including the appropriate header-“Phone Book” if the search string is empty (raw list being displayed) or “Search Results” if the search string is not empty (search results being displayed). This helps guarantee that the result set returned will be the same, allowing consistent paging through the set (with the caveat explained above).

Line 32 begins the <select> list, with lines 34-70 building and displaying five items as <option>s. Lines 35-38 build the text for the <option> prompt, while lines 39-44 construct the <option> statement with an appropriate “onpick” parameter that recursively calls the script with $cmd equal to “Display” and the ID of the record to display. Note that $idx and $search are also passed to maintain their values through the recursive call, just in case we need them later.

Lines 49-64 build option number 6 in our select list. If there are more records to display ($total_rows >= $next) the script generates the option “[Next Records]” with an appropriate “onpick” parameter to recursively call the script, specifying the next starting record to display (via idx=$next). If there are no more records to display, the script generates an “[End of List]” option, which recursively calls the script with the same starting point as is currently displayed. Each option also includes the search criteria if it exists (lines 51-54 and 58-61).

Finally, a “Home” option is created (lines 65-68) to allow the user to return to the home menu from any page of the listing. The open tags are then closed.

Search

The search function is simply an input tag that accepts up to 10 characters and recursively calls the script supplying the text entered and sets $cmd equal to “List.”

  echo "<card id="Search">n";
  echo "<do type="accept" label="Go">".$lf;
  echo "<go href="?cmd=List&amp;search=$searchtext">".$lf;
  echo "</go>".$lf;
  echo "</do>".$lf;

  echo "<p>".$lf;
  echo "<b>Phone Book Search</b><br/>Search for:".$lf;
  echo "<input name="searchtext" title="Search" type="text"";
  echo "  format="10m"/>";
  echo "</p>".$lf;
  echo "</card>".$lf;

Display

Display uses the record ID passed in $idx to select a record from the database and display all of its related information (name, address, phone, etc.).

  echo "<card>n";
  
  // Get specific record
  $query = "select * from Phone where Id = "".$idx.""";
  
  $result = mysql_query($query,$link)
    or die("Query failed:$query");
  
  // Get data and display
  while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
    $recordid = $line[Idx];

    echo "<p mode="wrap">".$lf;
    echo "$line[LastName], $line[FirstName]<br />".$lf;
    echo "$line[Address]<br />".$lf;
    echo "$line[City], $line[State]  $line[Zip]<br />".$lf;
    echo "<a href="wtai://wp/mc;$line[Phone]" title="Dial">";
    echo "$line[Phone]</a><br />".$lf;
    echo "<a href="?cmd=Menu" title="Menu">";
    echo "[Home Menu]</a><br /><br />".$lf;

    $Date = date("M j, Y", strtotime($line[LastUpdate]));
    echo "Record Updated:<br />$Date";
    
    // Close record display card
    echo $lf."</p></card>".$lf;
    
  }

Note that the card has a “Dial” option mapped to the Accept key and is displayed with the phone number highlighted. This allows the user to quickly dial the selected number on devices that support URL dialing. Other items of note include a “Home Menu” link and a more verbose format for the last update date. We do not need to provide any functionality to return to the last page of record listings-the user can do so by pressing the Back key on his or her device.

The Entire Script

Now that we’ve defined the various functions of the script, let’s tie it all together with a “switch” statement and some additional initialization statements:

<?php

header("Content-type: text/vnd.wap.wml");  

header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");              
// expires in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");   
// Last modified, right now
header("Cache-Control: no-cache, must-revalidate");              
// Prevent caching, HTTP/1.1
header("Pragma: no-cache");                                
// Prevent caching, HTTP/1.0

echo "<?xml version="1.0"?>n";  
echo "<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN""  
   . " "http://www.wapforum.org/DTD/wml_1.1.xml">n"; 
echo "<wml>n";

// Open link to DB
$link = mysql_connect("localhost", "webuser", "webby99")
  or die("Could not connect to database!");
mysql_select_db("customers")
  or die("Could not select database!");

// Line feed
$lf = chr(10);

// Make sure that index into DB has value
if (empty($idx)) { $idx = 0; }

if (empty($cmd)) { $cmd = "Menu"; }

switch ($cmd) {

   case "Menu";

      echo "<card id="Menu">n";
      echo "<p mode="nowrap">".$lf;
      
      // Set up Select menu list 
      echo "<select name="Select" title="Select:">".$lf;

      // Go through results from Query, listing each as a CHOICE entry
      echo "<option onpick="?cmd=List">";
      echo "List Contacts</option>".$lf;
      echo "<option onpick="?cmd=Search">";
      echo "Search Contacts</option>".$lf;
      
      // Close select
      echo "</select>".$lf;

      // Close card
      echo "</p>".$lf."</card>".$lf;

   break;

   case "List";

      // Construct appropriate *count* query
      $query = "select count(*) from Phone";
      if (!empty($search)) {
         $query = $query." where FirstName like "%".$search."%" or";
         $query = $query." LastName like "%".$search."%"";
      }

      $result = mysql_query($query,$link)
        or die("Query failed:$query");

      list($total_rows) = mysql_fetch_array($result);

      // Construct appropriate query
      $query = "select * from Phone";
      if (!empty($search)) {
         $query = $query." where FirstName like "%".$search."%" or";
         $query = $query." LastName like "%".$search."%"";
      }

      // Get first/next five records
      $query = $query." order by LastName limit ".$idx.",5";

      $result = mysql_query($query,$link)
        or die("Query failed:$query");

      // Advance DB index
      $next = $idx + 5;
      
      // Start card
      echo "<card id="Contacts">n";
      echo "<do type="accept" label="View"> <go href=""/> </do>".$lf;
      
      // Display appropriate full/search heading
      if (empty($search)) {
         echo "<p mode="nowrap"><b>Phone Book</b>".$lf;
      } else {
         echo "<p mode="nowrap"><b>Search Results</b>".$lf;
      }
      
      // Set up Select list (list of five records)
      echo "<select name="View" title="View:">".$lf;

      // Go through results from Query, listing each as a CHOICE entry
      while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {

        $recordid = $line[Id]; 
        $Name = $line[LastName] . ", " . $line[FirstName];
        $Number = $line[Phone];
        $Prompt = $Name . " (" . $Number . ")";

        // Build URL for option, include DB index and search
        $option = "<option onpick="";
        $option = $option."?cmd=Display&amp;id=".$recordid."&amp";
        $option = $option.";idx=".$recordid."&amp;search=".$search."">";
        $option = $option.$Prompt."</option>".$lf;
        echo $option.$lf;

      }

      // If there are more records to display, set up paging
      //   else mark end of list (to keep Home as same option)
      if ($total_rows >= $next) {

         // Link to next five
         echo "<option title="Next" onpick="?cmd=List&amp;idx=$next";

         // Pass Search criteria if exists
         if (!empty($search)) {
            echo "&amp;search=".$search;
         }

         echo "">[Next Records]".$lf."</option>".$lf;
         
      } else {
      
         echo "<option title="End of List" onpick="?cmd=List&amp;idx=$idx";
         
         // Pass Search criteria if exists
         if (!empty($search)) {
            echo "&amp;search=".$search;
         }

         // Close tags
         echo "">[End of List]".$lf."</option>".$lf;
         
      }         

      // Add option for Home
      echo "<option onpick="?cmd=Menu" title="Home">".$lf;
      echo "[Back to Home]".$lf;
      echo "</option>".$lf;

      // Close select
      echo "</select>".$lf;

      // Close card
      echo "</p>".$lf."</card>".$lf;
      
   break;
   
   
   case "Search";
   
      echo "<card id="Search">n";
      echo "<do type="accept" label="Go">".$lf;
      echo "<go href="?cmd=List&amp;search=$searchtext">".$lf;
      echo "</go>".$lf;
      echo "</do>".$lf;

      echo "<p>".$lf;
      echo "<b>Phone Book Search</b><br/>Search for:".$lf;
      echo "<input name="searchtext" title="Search" type="text"";
      echo "  format="10m"/>";
      echo "</p>".$lf;
      echo "</card>".$lf;
   
   break;
   
   
   case "Display";
   
      echo "<card>n";
      
      // Get specific record
      $query = "select * from Phone where Id = "".$idx.""";
      
      $result = mysql_query($query,$link)
        or die("Query failed:$query");
   
      // Get data and display
      while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
        $recordid = $line[Idx];

        echo "<p mode="wrap">".$lf;
        echo "$line[LastName], $line[FirstName]<br />".$lf;
        echo "$line[Address]<br />".$lf;
        echo "$line[City], $line[State]  $line[Zip]<br />".$lf;
        echo "<a href="wtai://wp/mc;$line[Phone]" title="Dial">";
        echo "$line[Phone]</a><br />".$lf;
        echo "<a href="?cmd=Menu" title="Menu">";
        echo "[Home Menu]</a><br /><br />".$lf;

        $Date = date("M j, Y", strtotime($line[LastUpdate]));
        echo "Record Updated:<br />$Date";
        
        // Close record display card
        echo $lf."</p></card>".$lf;
        
      }

   
   break;
   
}

echo $lf."</wml>".$lf;

mysql_close($link);

?>

Note that we pass a handful of headers at the beginning of the script to inhibit caching. Since our database is frequently updated and correct/up-to-date information in the field is valuable, we do not want the device to display cached information instead of recently updated information. However, generally speaking, inhibiting the cache is a bad idea and should be done sparingly, if at all.

The Script in Action

Now let’s see the script in action. The following figures demonstrate each function:

Note: All images courtesy Openwave Systems Inc. (Openwave, the Openwave logo, Openwave SDK, Openwave SDK Universal Edition, Openwave SDK WAP Edition are trademarks of Openwave Systems Inc. All rights reserved.)

Figure 1 – The menu.

Figure 2 – The list. Notice the [Next Records] option.

Figure 3 – The end of the list. Notice the [End of List] option.

Figure 4 – The search form.

Figure 5 – The results of a search (for “in”). Note the first result is “Biggins, Lisa” but the text has scrolled to the phone number due to the “nowrap.”

Figure 6 – A record in the display card.

Room for Improvement

This script has plenty of room for improvement, including the following items:

  • Applying the cache inhibitor headings only to cards that could cause problems (such as Display), instead of globally.
  • Optimizing the output to avoiding duplicating code (such as the addition of search criteria to the URL(s)).
  • Adding more prompts for the user through card titles, etc.
  • Optimizing and standardizing variable naming and usage.
  • Providing means for the user to edit records. Although it can be tedious to enter data on most mobile devices, simple corrections or notes would be welcome. A “last called” field could also be entered automatically each time a contact is called.

This script represents only a small portion of what can be done with PHP and a database such as MySQL. This example could be expanded to offer group calendaring, scheduling, order placement, stock checking, etc. As long as you keep the target audience and the respective design goals in mind, the sky’s the limit.

About the Author

Steve Schafer is president and CEO of Progeny Linux Systems, a Linux-based consulting company in Indianapolis, Indiana. He has written several technical books and articles and can be reached at sschafer@synergy-tech.com.

About the “Learning WML” series…

This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers advanced integration with PHP.

These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series.

# # #

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories