murz.net

on constructing the web.

About RSS
  • Automatically picking up changes to a Go project

    Lately I've been writing some web apps in Go. I really like Go for web development, but I miss having my changes dynamically compiled. The Go compiler is really fast but it's still a hassle to restart the server every time I tweak something in my web app. So I did what any sane Gopher would do, I wrote a Go program to watch my filesystem for changes and re-compile my web server every time I make an edit!

    Watching the filesystem on any OS is easy to do thanks to a third-party fsnotify package.

    go get github.com/howeyc/fsnotify

    I use exec.Command to start my web server in the background on a new goroutine, then I use fsnotify to poll for changes. When the fsnotify channel receives a filesystem event I kill the child process, re-compile it, and start it back up again.

    package main
    
    import (
    	"github.com/howeyc/fsnotify"
    	"io"
    	"log"
    	"os"
    	"os/exec"
    )
    
    func checkErr(err error) {
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    
    func main() {
            // The path to the web server executable binary.
    	var exepath string
            // Keep a pointer to the running command so we can kill it's process later.
    	var cmd *exec.Cmd
    
            // Function to build the executable binary inside the temp directory.
    	compile := func() {
    		exepath = path.Join(os.TempDir(), "myserver")
    		_, err := exec.Command("go", "build", "-o", exepath).Output()
    		checkErr(err)
    	}
    	
            // Function to start the executable then wire-up stdout and stderr.
    	start := func() {
    		compile()
    		cmd = exec.Command(exepath)
    		stdout, err := cmd.StdoutPipe()
    		checkErr(err)
    		stderr, err := cmd.StderrPipe()
    		checkErr(err)
    		go io.Copy(os.Stdout, stdout)
    		go io.Copy(os.Stderr, stderr)
    		err = cmd.Start()
    		checkErr(err)
    		err = cmd.Wait()
    	}
    
            // Function to kill the running process.
    	stop := func() {
    		cmd.Process.Kill()
    	}
    
            // Kick off the start function in the background.
    	go start()
    
            // Setup the filesystem watcher.
    	watcher, err := fsnotify.NewWatcher()
    	checkErr(err)
    	err = watcher.Watch(".")
    	checkErr(err)
    
            // Wait for events from the filesystem.
    	for {
    		select {
    		case evt := <-watcher.Event:
                            // When the filesystem has changed, stop the binary and start it again.
    			stop()
    			go start()
    		case err := <-watcher.Error:
    			log.Println("error:", err)
    		}
    	}
    }

    Note that I actually do a go build and then run the resulting binary (instead of using go run). This is because go run will actually fork a separate process which is harder to kill. See this mailing list discussion for more details.

  • Mustache templates in the Play! framework

    I just published my first Play! module to the official repository, it's called play-mustache. The aptly named module provides some integration between Mustache and the Play! framework. The goal is to make it easier to re-use markup across all layers of a dynamic web application.

    You can take a look at the code and read the manual on github. For a complete reference of the mustache template language checkout their manual.

    The markup snippets can be defined in an external file, but the module also provides a tag to define the snippets in a view. To define snippets inline you use the mustache.template tag.

    #{mustache.template 'task_item'}
    	<li class="task" id="task_{{id}}">
    		<span class="name">Task {{id}}</span>
    		<span class="details">{{details}}</span>
    		<span class="priority">{{priority}}</span>
    	</li>
    #{/mustache.template}

    The template can then be used in your view using the mustache.print tag.

    <ul id="tasks">
    #{list tasks, as: 'task'}
    	#{mustache.print 'task_item', context:task /}
    #{/list}
    </ul>

    And it can be used within your JavaScript using the PlayMustache.to_html method.

    var data = {
    	id: 34,
    	details: 'Finish the project',
    	priority: 'High'
    }
    var html = PlayMustache.to_html("task_item", data);
    $('#tasks').append($(html));

    If you don't want to specify your snippet inline, you can move it into an external file. By default the module will look in app/views/mustaches for template files, but you can configure this path by specifying mustache.dir in your application.conf.

    To reference an external snippet, just use the relative filename as the template name. For example, if your file is called app/views/mustaches/my_template.html then you would use it server-side like this:

    #{mustache.print 'my_template.html', context:data /}

    And client-side like this:

    PlayMustache.to_html('my_template.html', data);
  • IETest User Password

    For a while now, Microsoft has offered a free suite of Windows VirtualPC images, which enable web developers to test their web applications in legacy versions of Internet Explorer.

    If you've ever used one of the images you will know that if you alt+tab away and let the image idle, eventually windows brings you to the locked screen, which you cannot unlock without knowing the password to the account "IETest".

    Screenshot of the WindowsXP Locked Screen

    Unfortunately Microsoft has done a pretty poor job of communicating what the password is, but after a bit of digging I discovered that the password to IETest is:

    P2ssw0rd

    Hopefully that saves you some hair pulling.

  • Creating a responsive chat room using old-school AJAX and PHP

    With all of the "new school" JavaScript frameworks out there like jQuery and Dojo, it's extremely easy to get AJAX HTTP requests going without even knowing what the XMLHttpRequest object is. However, I think it's still important to understand what kind of magic jQuery is doing underneath the hood. So here is a beginner's tutorial I've recently resurrected and revised that describes how to create a responsive chat room using JavaScript and PHP.

    Before we begin, you can get the example code here and checkout the demo here.

    We're going to start out with our database model. Since this is a chat room we will probably need to keep track of the users in the room and all of the messages people have sent. After fleshing out the tables I came up with something like this:

    CREATE DATABASE IF NOT EXISTS chatroom;
    USE chatroom;
    --
    -- Definition of table `messages`
    --
    DROP TABLE IF EXISTS `messages`;
    CREATE TABLE `messages` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `userId` int(10) unsigned NOT NULL,
      `message` varchar(255) NOT NULL,
      `dateCreated` datetime NOT NULL,
      PRIMARY KEY  (`id`),
      KEY `FK_messages_1` (`userId`),
      CONSTRAINT `FK_messages_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`)
    )
    --
    -- Definition of table `users`
    --
    DROP TABLE IF EXISTS `users`;
    CREATE TABLE `users` (
      `id` int(10) unsigned NOT NULL auto_increment,
      `username` varchar(45) NOT NULL,
      `isActive` tinyint(1) NOT NULL,
      `dateJoined` datetime NOT NULL,
      `dateUpdated` datetime NOT NULL,
      `ipAddress` varchar(25) NOT NULL,
      PRIMARY KEY  (`id`)
    )

    Now that our tables are defined we can get started on a basic web service that our JavaScript code will use to communicate with the server. To keep this tutorial short and sweet we will be creating the service in PHP using PHP Data Objects (PDO). So create a new file called chat.php and setup your MySQL connection:

    $dbh = new PDO("mysql:host=localhost;dbname=chatroom", "YOUR_DB_USERNAME", "YOUR_DB_PASSWORD");

    The main purpose of this basic service will be to recieve new messages and to return the current state of our chatroom. However, we also need to recieve new users so lets handle that scenario first. Since this is not exactly a high-security chat room we won't have a lengthy registration process. All we really care about is the username. If there is a POSTed username on the request, we check if that username already exists. If it does, we assign this new IP to that username. If it doesn't, we simply push a new user to the database with the specified username. Be sure to use prepared statements to prevent MySQL injection attacks.

    /* persist user if theres a username on the request */
    if(isset($_POST["username"]) && strlen(trim($_POST["username"])) > 0){
      $dateNow = date("Y-m-d H:i:s");
      $ip = $_SERVER["REMOTE_ADDR"];
        
      $psUsernameCheck = $dbh->prepare("SELECT * FROM `users` WHERE `username` = ? AND `isActive` = 0 limit 1");
      $psUsernameCheck->execute(array($_POST['username']));
      if($psUsernameCheck->rowCount() > 0){
        $user = $psUsernameCheck->fetchObject();
        if(!$user->isActive){
          $psUpdateUser = $dbh->prepare("UPDATE `users` SET `ipAddress` = ?, `dateUpdated` = ?, `isActive` = 1 WHERE `id` = ? limit 1");
          $psUpdateUser->execute(array($ip,$dateNow,$user->id));
        }
        outputStatus();
      }else{
        /* otherwise, insert a new one */
        $psNewUser = $dbh->prepare("INSERT INTO `users` (`username`,`isActive`,`dateJoined`,`dateUpdated`,`ipAddress`) VALUES (?, ?, ?, ?, ?)");
        $isActive = true;
        $dateJoined = $dateUpdated = $dateNow;
        $psNewUser->execute(array($_POST['username'], $isActive, $dateJoined, $dateUpdated, $ip));
        outputStatus($dbh);
      }
    }

    Now that we have our extremely basic registration system in place, we will setup an even more basic authentication system. If there's a userId posted we will look it up in the database and make sure that the IP is the same as the one making the request. If it is then the user appears to be authentic so we update their dateUpdated column and move on.

    if(isset($_POST["userId"]) && is_numeric($_POST["userId"])){
      $psUserIdCheck = $dbh->prepare("SELECT * FROM `users` WHERE `id` = ? AND `isActive` = 0");
      $psUserIdCheck->execute(array(intval($_POST["userId"])));
      if($psUserIdCheck->rowCount() > 0){
        $user = $resUserIdCheck->fetchObject();
        if($user->ipAddress == $_SERVER["REMOTE_ADDR"]){
          $psUpdateUser = $dbh->prepare("UPDATE `users` SET `dateUpdated` = ? WHERE `id` = ?");
          $psUpdateUser->execute(array(date("Y-m-d H:i:s"),$user->id));
        }
      }
    }

    Now that we have an authentic user we can modify our basic service to handle incoming messages. This process will be as easy as checking for a message on the request and persisting it if it's there.

    if(isset($_POST["message"]) && strlen(trim($_POST["message"])) > 0){
      $id = null;
      $userId = intval($_POST["userId"]);
      $message = $_POST["message"];
      $dateCreated = date("Y-m-d H:i:s");
              
      $psNewMessage = $dbh->prepare("INSERT INTO `messages` VALUES (?, ?, ?, ?)");   
      $psNewMessage->execute(array($id,$userId,$message,$dateCreated));
    }

    Then we need to provide a way for the user to log out. We like short and sweet so we will just look for a posted value and log the user out if they are authentic and the value is there.

    if($_POST["logout"] == "true"){
      $dateNow = date("Y-m-d H:i:s");
      $psUpdateUser = $dbh->prepare("UPDATE `users` SET `dateUpdated` = ?, `isActive` = 0 WHERE `id` = ?");
      $psUpdateUser->execute(array($dateNow,$user->id));
    }

    And finally, we will dump the current status of the chatroom in xml format.

    $xml = "<?xml version='1.0' encoding='utf-8'?>" . "\n";
    $xml .= "<chatroom>\n";
    $xml .= "<users>\n";
    $psAllUsers = $dbh->query("SELECT * FROM `users` WHERE `isActive` = 1");
    while($resUser = $psAllUsers->fetchObject()){
      $xml .= "<user>\n";
      $xml .= "<username>".$resUser->username."</username>\n";
      $xml .= "</user>\n";
    }
    $xml .= "</users>\n";
    $psNewMessages = $dbh->prepare("SELECT * from `messages` where `dateCreated` > ?");
    $psNewMessages->execute(array($user->dateUpdated));
    $xml .= "<messages>\n";
    while($newMessage = $psNewMessages->fetchObject()){
      $psUser = $dbh->prepare("SELECT `username` FROM `users` WHERE `id` = ? limit 1");
      $psUser->execute(array($newMessage->id)); 
      $user = $psUser->fetchObject();
      $xml .= "<message>\n";
      $xml .= "<author>".$user->username."</author>\n";
      $xml .= "<content>".$newMessage->message."</content>\n";
      $xml .= "</message>\n";
    }
    $xml .= "</messages>\n";
    $xml .= "</chatroom>\n";
    echo $xml;

    Now that we're done with the service we can move to the front end. We will start by defining our markup in a new html file called index.html.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
        <title>AJAX Chatroom</title>
        <script type="text/javascript" src="chat.js"></script>
      </head>
      <body>
        <div id="chatroom">
          <div id="messages">
          </div>
          <div id="users">
            <fieldset>
              <legend>Users (<span id="user_count">0</span>)</legend>
              
            </fieldset>
          </div>
          <form id="form" action="chat.php" method="post">
            <p><input type="text" id="message" /></p>
            <p><input type="submit" value="Send" /></p>
          </form>
        </div>
        <script type="text/javascript">
          init();
        </script>
      </body>
    </html>

    Notice the JavaScript at the bottom of the document. That call to init will be the entry point for our client side scripts, it's at the bottom because it has to be called after the rest of the document has loaded. After we've got the markup ready to go we'll finally get started on the JavaScript. Create a new JavaScript file called chat.js and get started by defining an attachEvent function. Attaching an event is a little tricky in JavaScript because it's implemented differently depending on the user's browser. Checking which method to use is simple enough, but it's something we will do a lot so lets stuff it in a function.

    function addEvent(obj, evType, fn, useCapture){
      if (obj.addEventListener){
        obj.addEventListener(evType, fn, useCapture);
        return true;
      } else if (obj.attachEvent){
        var r = obj.attachEvent("on"+evType, fn);
        return r;
      } else {
        alert("Handler could not be attached");
      }
    }

    To create HTTP requests using JavaScript we have to use the XMLHttpRequest object which has the same problem as event handling. IE5.x and IE6 have implemented it differently. Again, since we use this everywhere, we will write another function to get an XMLHttpRequest object that accomodates everyone.

    function getXmlHttp(){
      var xmlhttp;
      if (window.XMLHttpRequest){
          // If IE7, Mozilla, Safari, etc: Use native object
        xmlhttp = new XMLHttpRequest();
      }
      else{
         // ...otherwise, use the ActiveX control for IE5.x and IE6
          xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
      return xmlhttp;
    }

    Now lets create a function that will initialize the chatroom. The first thing we need to do when we initialize is ask the user for their username. Once they give it to us we will need to send it up to the server using an XMLHttpRequest object. The process is fairly simple, we open the request, set our headers, then call send passing our parameters. Then we add a listener to the object's onreadystatechange event, the readyState value we are looking for is 4 because that means the response is ready. You can read more about the readyState property over at quirksmode. After we know there's a response we check the status property which exposes the HTTP status code our server returned. We're looking for a 200 OK status code, which means the operation was successful. You can read more about HTTP status code's from at the W3C's website.

    function init(){
      var username = prompt("Please enter your username...");
      xmlhttp = getXmlHttp();
      xmlhttp.open('POST', 'chat.php', true);
      xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      xmlhttp.send('username=' + username);
      xmlhttp.onreadystatechange = function(){
        if (xmlhttp.readyState == 4) {
          if (xmlhttp.status == 200) {
            /* everything went ok, lets parse! */
          }
        }
      }
    }

    If no one else is using the username the server will send us down some xml, otherwise the response will be empty and we will tell the user that they were not logged in.

    if (xmlhttp.responseXML != null) {
      /* log in successful */
    }else{
      alert("you were not logged in!");
    }

    In the case that we get some xml, lets start to parse it. We will use the DOM element methods like getElementById, getElementsByTagName, and createElement to parse out what we need from the XML DOM and place it into our HTML DOM. For an in-depth reference of Gecko DOM methods I highly recommend Mozilla Developer Center. Lets start simple by just grabbing the current user id and storing it in a variable.

    currentUser = responseXML.getElementsByTagName('currentUser')[0].firstChild.data;

    Then we will get the list of users and add each user to the unordered list inside the user fieldset.

    users = new Array(xmlhttp.responseXML.getElementsByTagName('user').length);
    document.getElementById("user_count").innerHTML = users.length.toString();
    var userList = document.createElement("ul");
    for (var i = 0; i < users.length; i++) {
      var user = xmlhttp.responseXML.getElementsByTagName('user')[i];
      var username = user.getElementsByTagName("username")[0].firstChild.data;
      var userItem = document.createElement("li");
      userItem.innerHTML = username;
      userList.appendChild(userItem);
    }
    document.getElementById("users").getElementsByTagName("fieldset")[0].appendChild(userList);

    After we get the users updated, lets parse out the messages and add them to our message list.

    messages = new Array(xmlhttp.responseXML.getElementsByTagName('message').length);
    var messageList = document.getElementById("message_list");
    for (var i = 0; i < messages.length; i++) {
      var message = xmlhttp.responseXML.getElementsByTagName('message')[i];
      var author = message.getElementsByTagName("author")[0].firstChild.data;
      var content = message.getElementsByTagName("content")[0].firstChild.data;
      var messageItem = document.createElement("li");
      messageItem.innerHTML = "<strong>"+author+":</strong> "+content;
      messageList.appendChild(messageItem);
    }

    The process of extracting the current chatroom state from XML and updating the DOM is one thats going to happen quite a bit, so lets move it into a method called updateDOM that takes the value of xmlhttp.responseXML as a parameter. Now lets move on and start building our synchronization function. We will basically just send a request and call updateDOM with the response every five seconds.

    function synch(){
      xmlhttp = getXmlHttp();
      xmlhttp.open('POST', 'chat.php', true);
      // Send the POST request
      xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      xmlhttp.send('userId=' + username);
      xmlhttp.onreadystatechange = function(){
        if (xmlhttp.readyState == 4) {
          if (xmlhttp.status == 200) {
            if (xmlhttp.responseXML != null) {
              updateDOM(xmlhttp.responseXML);
            }else{
              alert("you were not logged in!");
            }
          }
        }
      }
      updateDOM(xmlhttp.responseXML);
      setTimeout('synch()',3000);
    }

    Be sure to modify your init method to call synch after you recieve the initial response. Then get started on the send method, which will be called when our message form is submit. This method needs to take the value from the message text field, make sure it's not blank, and post it to the server. Immediately after we send the request we will clear the form.

    function send(e){
      e.preventDefault();
      var message = document.getElementById("message").value;
      if(message == null || message == ""){
        alert("You must enter a message!");
        return;
      }
      xmlhttp = getXmlHttp();
      xmlhttp.open('POST', 'chat.php', true);
      // Send the POST request
      xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      xmlhttp.send('userId=' + currentUser + '&message=' + message);
      xmlhttp.onreadystatechange = function(){
        if (xmlhttp.readyState == 4) {
          if (xmlhttp.status == 200) {
            if (xmlhttp.responseXML != null) {
              updateDOM(xmlhttp.responseXML);
            }else{
              alert("Message could not be sent!");
            }
          }
        }
      }
      document.getElementById("message").value = "";
    }

    The final javascript function we ned to write is the logout function. This function will send a post request to chat.php just like the others, the only difference will be that this request will have logout=true.

    function logout(){
      xmlhttp = getXmlHttp();
      xmlhttp.open('POST', 'chat.php', true);
      xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      xmlhttp.send('userId=' + currentUser + '&logout=true');
      xmlhttp.onreadystatechange = function(){
        //we can't really do anything except hope it worked
      }
    }

    Now lets create a function to wire up all the events call it at the end of init.

    function bindEvents(){
      addEvent(document.getElementById("form"),"submit",send,false);
      addEvent(window,"unload",logout,false);
    }

    We are almost done, but there's one more modification I'd like to make. Since we can't reliably count on the logout function always being called, lets modify chat.php so that it clears "stale" users with every request. We will consider any user that hasn't been updated in the past day to be "stale".

    $yesterday = date("Y-m-d H:i:s",strtotime("yesterday"));
      $psClearStaleUsers = $dbh->prepare("UPDATE `users` SET `isActive` = 0 WHERE `dateUpdated` <= ?");
      $psClearStaleUsers->execute(array($yesterday));

    And there you have it! Your own responsive chat-room that you built using just the bare essentials, congratulations! With just a tiny bit of CSS you can make your chat-room look like this (if you're interested in that CSS it's in the example code zip).

  • How to fix common CSS bugs in IE6

    Today we take on an opponent that few dare to challenge. It's the arch nemesis of web developers across the globe, it's antics have caused countless hours of sheer agony, and it's mere presence will mutilate even the most standards-compliant designs. You better believe I'm talking about the infamous anti-browser known as Internet Explorer 6, and today I am going to share a few tricks I use to beat it into submission.

    Okay, so maybe it's not quite evil, but what it does to some designs is just plain wrong. And I've recently learned that it still holds close to twenty-percent of the market share by most estimates. This is pretty surprising considering version 7 has now been out for three years and version 8 is already in beta testing. But what does it mean for web developers? It means we have to get over our fears and learn to play nice with the vicious monster in the corner. And just in case the monster bites back, I've put together a list of a few common bugs and fixes that every web developer should have in their arsenal. These are just some tricks I've picked up after fixing a handful of designs in Internet Explorer 6.

    • Targeting the monster

      There are several CSS hacks out there that allow you to apply style to IE6 without effecting other browsers. In my opinion, the safest and cleanest way is to use conditional comments. The following markup is treated as a simple HTML comment when parsed in most browsers:

      <!--[if lte IE 6]>
      <link rel="stylesheet" type="text/css" href="/style/ie6.css" />
      <![endif]-->

      However, most versions of Internet Explorer will recognize the conditional statement inside the comment. If the browser meets the condition, it will treat the content as if it was regular HTML. So we can easily use the conditional comment to grab a specific external stylesheet for anyone viewing the site in IE6 or earlier, while keeping our markup and CSS virtually the same for all other users. For an overview of all the availible conditional comments, take a look at this article.

    • Double the margins, double the fun!

      This is probably the most common problem I see when I first pull down my sites from IE6. All of my div's appear to be sized and positioned in odd ways, usually due to a known bug in Internet Explorer 6 that causes the browser to double padding and margins on certain floated divs. When I first encountered this problem I would go through and redefine all of the problem-divs' left and right margins to half of what they should be. This worked okay, but it was obviously a pain to go through and redeclare the margins for all of the effected divs. After stumbling across an article on the subject I learned that the double margin effect can be countered by adding the following css property to all of the problem-divs:

      #an-ie6-victim {display:inline;}

      Browsers that follow W3C standards will ignore the display:inline attribute on any floated div, but for some reason it forces IE6 to render your div with correct margins and padding. Although very strange, the fix works. And I like it better than my original approach because I don't have to redefine all of my margins.

    • Standards, shmandards!

      After fixing the double margins problem, you may still see some oddly sized elements on your page. This is probably due to Internet Explorer's faulty box model. Basically, Internet Explorer will treat an element's width definition as the width of it's content, padding, and border. This is in contrast to the W3C's box model which treats an element's width definition as the width of it's content only. The following diagram clearly illustrates the difference:

      Diagram illustrating IE6 box model
      Diagram illustrating the box model mismatch.

      Unfortunately there isn't an obscure attribute we can define to magicly fix the problem this time. I recommend using a conditional comment to redefine your element's width when the page is rendered with the IE box model. Other developers will recommend that you modify your markup and styles to avoid the issue altogether. For further reading about this problem, I strongly suggest you check out this article by Roger Johansson.

    • You want quirks? I'll give you quirks...

      This is one I didn't run into until later in my career. Apparantly when IE6 is pushed into quirks mode it completely neglects to recognize and calculate automatic margins (margin:0 auto 0 auto;). And to make the situation worse, Microsoft decided to add an extra quirks mode condition: IE6 will revert to quirks mode if there is anything preceding your doc-type declaration. This is a problem because it is very common practice to include an XML prologue before a doc-type definition. In fact, the W3C recommends that you do:

      XHTML document authors are strongly encouraged to use XML declarations in all their documents. Such a declaration is required when the character encoding of the document is other than the default UTF-8 or UTF-16.

      My solution is to check the request and if it's from a user-agent that is known to handle an XML declaration incorrectly (such as Internet Explorer 6), I will omit the declaration from my markup. It's probably a good idea to note that this is not a bullet-proof solution, because I'm sure there are user-agents out there that are unchecked by my server yet cannot handle an XML prologue. You may decide that physically removing the declaration from your markup is a better solution for your website. If you would like further reading on quirks mode, wander on over to the site named after it.

    • Show me the alpha!

      A 32-bit PNG has something called an alpha-channel, which can define a variable level of transparency for each pixel in an image (a feature known as per-pixel-transparency). The alpha channel allows web designers to do some really cool things with their graphics, but unfortunately it is not supported in IE6. Microsoft does offer a work-around in the form of a proprietary filter called AlphaImageLoader. Any element that uses a transparent PNG will need an additional attribute that looks something like this:

       
      filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/path/to/transparent-img.png');

      The filter attribute is not defined in the W3C's CSS specification and will make your CSS invalid. Since it only applies to IE6 or earlier, I highly recommend that you use a conditional comment to include any of your stylesheets that utilize the AlphaImageLoader.

    After fixing all of those bugs, I usually find that my page starts rendering like it should in IE6. The next step is to break out Firebug-Lite and start debugging the inevitable IE6 JavaScript bugs, but I'll save that for another article.

  • ← older entries
  • newer entries →
Fork me on GitHub