Christian Heilmann

You are currently browsing the Christian Heilmann blog archives for April, 2008.

Archive for April, 2008

Oh look, using Ajax in a stupid way is not a good idea?

Tuesday, April 29th, 2008

It is quite fascinating to me that the newest article on dev.opera.com entitled ‘Stop using Ajax!’ is such a big thing right now. Tweets, shared bookmarks and Google Reader items are pouring in and people seem to consider it an amazingly daring article.

Here’s the truth: James is right. He also was right when he more or less gave the same information as a talk at Highland Fling last year following my presentation on progressive enhancement and JavaScript.

However, there is nothing shocking or daring or new about this. All he says is:

  • Don’t use any technology for the sake of using it
  • Consider the users you want to reach before using a technology that may not be appropriate
  • Make sure your solution is usable and accessible
  • Build your solution on stuff that works, then enhance it.

This is what I consider to be a normal practice when developing any software or web solution.

However, the real question is now why we are at this state – how come that we see this information as daring, shocking or controversial, and how come a lot of comments are still “I don’t care about accessibility because it is not needed for my users”? How come the assumptions and plain accessibility lies are prevailing while the good stuff remains unheard of?

Well, the truth is that we have been preaching far too long to the choir. I’ve been in the web accessibility and standards preaching community for a long time and whenever I asked what about enterprise development and CMS I was told that it is not worth fighting that fight as “We will never reach them”. Well, this is where the money and a lot of jobs are and it is a fact that both accessibility and standards activists in a lot of instances don’t even know the issues that keep the stakeholders in these areas busy. My Digital Web Article ‘10 reasons why clients don’t care about accessibility’ and the follow-up Seven Accessibility Mistakes Part One and Part 2 listed these issues and the wrong ways of how we try to tackle them 3 years ago. My talk at the AbilityNet conference last week Fencing-in the habitat also mentioned this attitude and problems.

Here’s where I am now: I am bored and tired of people fighting the good fight by blaming each other’s mistakes or pointing out problems on systems that are within reach. When people ask for accessibility or Ajax usability advice you’ll get a lot of bashing and “go validate then come back” answers but not much information that can be used immediately or even questions that ask what lead to the state of the product. You’d be surprised what you can find out by asking this simple question.

We have to understand that large systems, frameworks and companies do still run the show, even when we think that bloggers, books on webdesign and mashups push the envelope. They do, but so far they are a minor discomfort for companies that sell Ajax and other out-of-the-box solutions that are inaccessible and to larger parts unusable for humans. When was the last time you used a clever expense or time tracking system in companies that are not a startup or a small web agency? When I was at the AjaxWorld conference in NYC earlier this year I heard a lot about security, ease of deployment and scalability but only a little bit about accessibility (the Dojo talk and the YUI talk, actually). People are a lot more concerned about the cost of software and the speed of release than about the quality or maintainability. It is cheaper to buy a new system every few years than to build one that is properly tested and works for all users. Does your company still have systems or third party solutions that only work on IE/Windows? I am sure there is at least one, ask the HR or finance department.

It doesn’t help to coin another term and call an accessible and usable Ajax solution Hijax, either. As much as I like the idea of it I have to agree with James’ comment – we don’t need another word, we need a reason for people to not just use things out of the box without thinking about them or – even better – offer help to the companies that build the solutions on assumptions in the first place. When I ranted about a system by a large corporation some weeks ago on twitter their marketing manager for EMEA starting following me and I am starting some talks with them.

I have heard numerous times that my ideas about progressive enhancement and accessibility are just a “passing fad” and “that in the real software market you don’t have time for that”. Challenging this attitude is what makes a difference – by proving that by using the technologies we are given in a predictable and secure way does save you time and money. However, there are not many case studies on that…

I cannot change the world when I don’t know what obstacles people have to remove to do the right thing. Deep down every developer wants to do things right, in a clean and maintainable fashion and be proud of what they’ve done. Bad products happen because of rushed projects, bad management and developers getting so frustrated that they are OK with releasing sub-par just to get the money or finally get allocated to a different project.

This is the battle we need to fight – where do these problems come from? Not what technology to avoid. You can use any technology in a good way, you just need to be able to sell it past the hype and the assumption that software is developed as fast as it takes to write a cool press release about it.

Summary about the Accessibility2.0 conference on YDN

Tuesday, April 29th, 2008

I just put up a summary blog-post about AbilityNet’s Accessibility 2.0 conference on the Yahoo Developer Network blog.

It is pretty tricky writing a summary of a conference you spoke at yourself, I found. In any case, I wanted to say thanks to everybody involved in organizing the conference and the other speakers. I learnt quite a bit myself at the conference and I’ll be happy to come around next year to see where we’ve come.

Fencing in the habitat – doing things right and getting the accessibility wrong

Saturday, April 26th, 2008

This is my talk given at AbilityNet’s Accessbility 2.0 conference in London today. In it I am talking about how we try to sell accessibility and the mistakes we make while we do so.

Hacking SlideShare’s embed adding a preview and be a lot shorter and readable

Thursday, April 17th, 2008

Edit: There is a bug in the script (see comments) but somehow Googlecode does not allow me to edit my own file. I will fix it once I got around that issue.The bug reported in the comments is now fixed, sadly enough I also had to re-write the converter as Google code does not allow me to replace an older version of a download (or is there a trick?). The new file is called previewer2.js

As readers of this blog know, I am a big fan of SlideShare as a distribution platform for my presentation slides. However, there are some things that annoy me about it.

One of them is the rather verbose embed code SlideShare offers you:



That is quite a mouthful and the main issue is that when you use several slide embeds in one document, you’ll slow down the rendering of your page as each of these Flash embeds need to be instantiated and tries to pre-cache the first three slides from S3.

I’ve analyzed the code a bit, added some other info I found in the RSS feed and came up with a small JavaScript that embeds slides in a different way. All you need there is the following code:



This gives slideshare the same SEO link love but is a lot less to add. Instead of the full slide include, you’ll get a preview image you can click that gets replaced with the flash movie. The following are examples:

Now, in order to convert one to the other you could do it by hand, or use the slideshare embed converter or install the Greasemonkey script

So far this is a hack, but I talked to Jonathan Boutelle about it yesterday night at the San Francisco JavaScript meetup and he is happy to pursue this idea further. My wishlist:

  • A larger preview image
  • A rest API call that gives me this information in a readable manner

Example of an unobtrusive, lazy-loading badge using the Twitter API

Friday, April 11th, 2008

Following questions I had about my talk at Highland Fling about badges for distribution and a twitter nagging by Tantek about the official twitter badge I thought I’d have a go at creating a twitter badge following some of the best practices I mentioned in my talk. Here’s the result.

The markup

Instead of HTML containers that will be seeded with real data when JavaScript is available and pointless when it isn’t, I wanted to build on top of HTML that makes sense without scripting and get all the info my script needs from there.





Example of a unobtrusive, lazy loading twitterbadge





In order to customise the badge, I allow for CSS classes with information to be added to the main container:





Example of a unobtrusive, lazy loading twitterbadge





They mean the following:

  • amount-n defines the amount of tweets to be displayed with n being an integer
  • skin-name defines the skin you want to use (for now this is grey and blue)
  • userinfo defines if the user’s avatar, name and location should be displayed.

The script

Here’s the full script and we’ll go through the bits one by one.

twitterbadge = function(){
var config = {
countDefault:5,
badgeID:’twitterbadge’,
userID:’twitterbadgeuser’,
tweetsID:’twitterbadgetweets’,
userinfo:’userinfo’,
stylesmatch:/skin-(w+)/,
amountmatch:/amount-(d+)/,
styles:{
‘grey’:’twitterbadge.css’,
‘blue’:’twitterbadgeblue.css’
}

};
var badge = document.getElementById(config.badgeID);
if(badge){
var link = badge.getElementsByTagName(‘a’)[0];
if(link){
var classdata = badge.className;
var head = document.getElementsByTagName(‘head’)[0];
var amount = config.amountmatch.exec(classdata);
var amount = amount ? amount[1] : config.countDefault;
var skin = config.stylesmatch.exec(classdata);
if(skin && skin[1]){
var style = document.createElement(‘link’);
style.setAttribute(‘rel’,’stylesheet’);
style.setAttribute(‘type’,’text/css’);
style.setAttribute(‘href’,config.styles[skin[1]]);
head.insertBefore(style,head.firstChild);
}

var name = link.href.split(‘/’);
var resturl = ‘http://twitter.com/statuses/user_timeline/’ +
name[name.length-1] + ‘.json?callback=’ +
‘twitterbadge.show&count=’ + amount;
var script = document.createElement(‘script’);
script.src = resturl;
script.type = ‘text/javascript’;
function show(result){
if(classdata.indexOf(config.userinfo) != -1){
var user = document.createElement(‘p’);
user.id = config.userID;
var img = document.createElement(‘img’);
img.src = result[0].user.profile_image_url;
img.alt = result[0].user.name;
user.appendChild(img);
var ul = document.createElement(‘ul’);
var data = [‘screen_name’,’name’,’location’];
for(var i=0;data[i];i++){
if(result[0].user[data[i]]){
var li = document.createElement(‘li’);
li.appendChild(document.createTextNode(result[0].user[data[i]]));
ul.appendChild(li);
}

}
user.appendChild(ul);
badge.appendChild(user);
}

var tweets = document.createElement(‘ul’);
tweets.id = config.tweetsID;
for(var i=0,j=result.length;i var username = result[i].user.screen_name;
var li = document.createElement(‘li’);
var span = document.createElement(‘span’);
span.innerHTML = result[i].text+’ ‘;
li.appendChild(span);
var link = document.createElement(‘a’);
link.setAttribute(‘href’,’http://twitter.com/’ + username +
‘/statuses/’+result[i].id);
link.appendChild(document.createTextNode(relative_time(result[i].created_at)));
li.appendChild(link);
tweets.appendChild(li);
}

badge.appendChild(tweets);
}

function relative_time(time_value) {
var values = time_value.split(” “);
time_value = values[1] + ” ” + values[2] + “, ” + values[5] + ” ” + values[3];
var parsed_date = Date.parse(time_value);
var relative_to = (arguments.length > 1) ? arguments[1] : new Date();
var delta = parseInt((relative_to.getTime() – parsed_date) / 1000);
delta = delta + (relative_to.getTimezoneOffset() * 60);
if (delta < 60) {
return ‘less than a minute ago’;
} else if(delta < 120) {
return ‘about a minute ago’;
} else if(delta < (60*60)) {
return (parseInt(delta / 60)).toString() + ’ minutes ago’;
} else if(delta < (120*60)) {
return ‘about an hour ago’;
} else if(delta < (24*60*60)) {
return ‘about ’ + (parseInt(delta / 3600)).toString() + ’ hours ago’;
} else if(delta < (48*60*60)) {
return ‘1 day ago’;
} else {
return (parseInt(delta / 86400)).toString() + ’ days ago’;
}

}
}

}
return {
show:show,
init:function(){
head.appendChild(script);
}

};
}();
twitterbadge.init();

I am using the revealing module pattern to keep code short and avoid global callback methods. However, there is a slight Opera oddity with generated script nodes in module patterns so we have to deviate from the norm there with an extra init() method call after the main module.

The first thing I thought of providing is a configuration object for the script. This makes it easy to change settings of it without having to hunt through the whole script and is just a nice service for the implementer:


twitterbadge = function(){
var config = {
countDefault:5,
badgeID:’twitterbadge’,
userID:’twitterbadgeuser’,
tweetsID:’twitterbadgetweets’,
stylesmatch:/skin-(w+)/,
amountmatch:/amount-(d+)/,
styles:{
‘grey’:’twitterbadge.css’,
‘blue’:’twitterbadgeblue.css’
}

};

Here we have all the IDs in use, the style names and the corresponding file names and the regular expressions to get the data from the CSS class name. All of the IDs and classes are hooks to define your own skins. There is also a countDefault variable to define how many items should be shown when the amount class is not set.


var badge = document.getElementById(config.badgeID);
if(badge){
var link = badge.getElementsByTagName(‘a’)[0];
if(link){

I test for the badge and that it contains a link as this is where we will get all our configuration data from.


var classdata = badge.className;
var head = document.getElementsByTagName(‘head’)[0];
var amount = config.amountmatch.exec(classdata);
var amount = amount ? amount[1] : config.countDefault;
var skin = config.stylesmatch.exec(classdata);
if(skin && skin[1]){
var style = document.createElement(‘link’);
style.setAttribute(‘rel’,’stylesheet’);
style.setAttribute(‘type’,’text/css’);
style.setAttribute(‘href’,config.styles[skin[1]]);
head.insertBefore(style,head.firstChild);
}

Then I am ready to read the information from the class. I set a shortcut to the document head and read the amount of tweets to be displayed. If there is no amount-n class set I fall back to the default.

Next is the skin, I check if the class was set and if that is the case I create a new link element pointing to the right skin. I get the href from the configuration styles object.

Notice that I use insertBefore() to add the style to the head of the document and not appendChild(). This ensures to a degree that the skin css file will not override settings that might be in other stylesheets. The last included style sheet rules them all.


var name = link.href.split(‘/’);
var resturl = ‘http://twitter.com/statuses/user_timeline/’ +
name[name.length-1] + ‘.json?callback=’ +
‘twitterbadge.show&count=’ + amount;
var script = document.createElement(‘script’);
script.src = resturl;
script.type = ‘text/javascript’;

Now it is time to find the user name (by splitting the href attribute of the link) and assemble the REST url to get the twitter data. Normally I would have added the new script node to the head directly aftwerwards, but Opera doesn’t like this.


function show(result){
if(classdata.indexOf(config.userinfo) != -1){
var user = document.createElement(‘p’);
user.id = config.userID;
var img = document.createElement(‘img’);
img.src = result[0].user.profile_image_url;
img.alt = result[0].user.name;
user.appendChild(img);
var ul = document.createElement(‘ul’);
var data = [‘screen_name’,’name’,’location’];
for(var i=0;data[i];i++){
if(result[0].user[data[i]]){
var li = document.createElement(‘li’);
li.appendChild(document.createTextNode(result[0].user[data[i]]));
ul.appendChild(li);
}

}
user.appendChild(ul);
badge.appendChild(user);
}

Now it is time to start the core functionality: the show method that will be invoked by the twitter REST API callback. I check if the userinfo has been set and create the markup accordingly. Nothing amazing here.


var tweets = document.createElement(‘ul’);
tweets.id = config.tweetsID;
for(var i=0,j=result.length;i var username = result[i].user.screen_name;
var li = document.createElement(‘li’);
var span = document.createElement(‘span’);
span.innerHTML = result[i].text+’ ‘;
li.appendChild(span);
var link = document.createElement(‘a’);
link.setAttribute(‘href’,’http://twitter.com/’ + username +
‘/statuses/’+result[i].id);
link.appendChild(document.createTextNode(relative_time(result[i].created_at)));
li.appendChild(link);
tweets.appendChild(li);
}

badge.appendChild(tweets);
}

Next I get the tweets information, assemble a list and add it to the badge.


function relative_time(time_value) {
[...]
}

}
}

return {
show:show,
init:function(){
head.appendChild(script);
}

};
}();
twitterbadge.init();

The relative_time method is actually taken from the original twitter badge and calculates how old the tweets are. I end the module with a return statement that defines the public methods (in this case only show) and add the script node to call the REST API in an init method. This is only necessary to fix the Opera issue.

Download and Example

You can download the twitter badge and see it in action.