Archive by Author

Facebook-Style Expanding Textboxes With jQuery

Characters I was recently asked to create a textbox that would vertically expand depending on how much was written in it. This style of text box can be seen in the Facebook news feed for writing comments under peoples feed items. When I receive this type of request, I usually begin by researching what existing systems and plug-ins are out there, investigate the different approaches people take and then write my own based on which way I think best.

Firstly, a textbox (input type=text) is the wrong element to be using. It cannot be any larger than a single line, so a text area element needs to be used. As a text area element will not automatically expand by itself, the general approach to this problem is to use a hidden ‘staging’ element placed way off the viewable screen. The text entered into the text area is copied across to the staging element, which expands as needed and the resulting height used to resize the text area. It is not a difficult thing to achieve with jQuery and the basic structure of the plug-in can be made in relatively few lines of code.

There are a few things to watch out for however. Firstly, because we are taking the contents of a text area and placing it in a div, we need to watch out for special characters that need encoding before they will render correctly. Primarily the newline character but we also have to watch out for the other special characters that can cause issues. Nothing that cannot be fixed with regular expressions. The other thing that we need to be careful of is the CSS styles that have been applied to the text area. We require that the hidden element perfectly mimic the text area and that the text wraps at the same point. If it does not, we will incorrectly predict when the text area needs to grow. The font size and family, line height and padding need to be copied.

This would normally be the point that I post the code, but after I started writing the plug-in, I stumbled across an existing one that I could just not improve upon and decided to use it in it’s entirety. Its by a guy called Jason Frame and his plug-in can be found on github. For my particular implementation, I added the facility to have watermark text in it by utilising the great plugin over on digital bush and I also added some text parsing requirements that were required for the particular project.

Now for a word of warning. There are some problems with this approach, which I have not managed to solve (and incidentally, nor have Facebook). The text area element renders it’s content slightly differently in every browser. In the hidden element, the text will sit up directly against the edge of the div but in a text area it may not. I suspect this gap also varies from operating system to operating system but as far as I can tell, you cannot eliminate it with styles. The consequence of this is that predicting precisely when the text area needs resizing becomes very difficult. The only thing to do is to accommodate this margin of error. This can be done by making the text area display slightly taller than a single line and to always have an extra line available. That way, if things go slightly off, it is still usable. You can see this problem if you put a significant amount of dummy text into the text area and watch when it expands as you type. Instead of expanding when you reach a new line it happens at a different place. If any one manages to figure out how to completely eliminate this problem I am eager to hear how.

Thanks for Breaking Things ASP.NET 3.5 Team

broken_glass I have just spent an intense few hours crawling all the JavaScript files in our application (and there are a quite a few) fixing an issue introduced with our upgrade to the .NET framework 3.5. The entire afternoon in fact was spent fixing a single issue: our AJAX calls had stopped working.

The first hurdle was due to a slightly off-the-norm setup we have for our website. In order to save replicating code across the various websites that make up our solution, we opted for a common virtual directory that serves common assets like JavaScript, images and so on. With a little jiggery pokery, it also allows us to use shared .ascx controls. When we upgraded to the 3.5 framework, the upgrade wizard scans the projects and changes any references that should be pointing the new 3.5 versions. Not all references were changed, as the latest framework sits on top of the 2.0 framework, rather than replaces it. Unfortunately, the upgrade wizard could not figure out that our virtual directory project was not an application at all, so it created an unneeded web.config for it . However, removing that made little difference and it took me quite a while until I found that there were additional script handler settings added for framework 3.5 that were causing the errors. We chose jQuery as the backbone of our JavaScript development and only need to have the .ascx web service handler.

As soon as I removed the unwanted handlers, our AJAX calls sprung into life but still none of our AJAX powered controls worked. A quick look with Firebug revealed the reason. All our JSON responses where wrapped in a {d: } object. A little searching on the interweb and the reason was revealed. As a security measure, wrapping the response in that way can stop cross-site scripting attacks.

So why am I annoyed? Well, I cannot fault the reason behind the change, it stops a potentially nasty attack. However, we had no choice in the matter. It broke every one of our AJAX powered controls. Just because Microsoft had handled this change in their own AJAX controls, they decided that nobody else would be using the JSON serializer for anything else. As such, they seem to have no configuration setting to turn it off. We had been running the 2.0 AJAX extension with that vulnerability in it for a while, so I think we should at least be allowed to choose whether we continue that way. Instead, I have to spend hours checking all the JavaScript file for AJAX calls and making them look for their data inside the d object.

Do You Speak HTML?

bubble Ever since I have been living away from my home country of England, I have noticed myself picking up on what I would consider misuses of the English language. On the most part, they are Americanisms, for which I take great pleasure and amusement in refusing to understand, until the person uses the English pronunciation. However, I also seem to hear an awful lot of completely made-up words or incorrect grammar. One may argue that this does not matter, as long as you can communicate the message but I guess being from the country that shares it’s name with the language, I have a built-in pride for it’s correct usage.

In development, things are usually not so forgiving when it comes to language. If you don’t get the spelling or grammar correct, it just will not compile or operate as expected. That is, except for HTML. Browser rendering engines are not fussy at all when it comes to HTML and will render pages happily, even when the code is all over the place. There is historical reason for this but unfortunately it seems to have lead to complete complacency in developers.

HTML is not a difficult language, it is simple to understand, there are not that many grammar rules and the vocabulary is tiny. So why then is there such a massive quantity of web developers who are just not that fluent in the language? I am not talking about just being able to get a page to look like the design. I am talking about expressing the content of the page using correct elements and combinations there of. There is a reason behind every html element, they were created because of a need, and each should be used where it is appropriate to do so.

If you are from the school of thought that does not worry about this, as long as it renders as the designer imagined, then there are good reasons why you should be thinking otherwise. Every day, web bots will be viewing your pages and trying to understand them. Screen readers do the same thing and a badly constructed page can be totally confusing to a blind person. You could be losing out on valuable traffic because of this. You could also find yourself with more work down the line. A site like Css Zen Garden shows just how much can be done with design without touching the html. That is not so easy when you have to work with a badly created page.

So what if that is not a big enough reason for you? Well, look at it this way. People who can speak correctly and have an extensive grammar, find themselves in a much better position than those who don’t. It can give a great first impression. The same thing is true for development. You may consider yourself pretty good in your chosen programming language but if you are sloppy in such a simple language like html, it could be the deal clincher in a job interview. It would certainly change my impressions of a potential candidate if they don’t have a decent grip on something that they should be able to in their sleep.

So how is yours? Time to brush up perhaps? Are you really that fluent? Be honest with yourself. How well do you speak html?

Edit: To all those that have taken this opportunity to pick holes in my use of language in this article: I do not consider myself an expert in English, by any means. I make as many mistakes as the next person. I now live away from England and on a daily basis I hear non-English people use phrases and pronunciations I was taught are incorrect. I don’t try to preach the way I was taught or consider that way the only way it should be done. My point was that something like Html should not have such variance. There is a worldwide standard we are all supposed to adhere to and were it not such a forgiving markup language, things would be much better.

Check your PHP UTF-8 Checklist

I spent way too long this weekend on a problem that had such a simple solution. I guess this issue may have been a little to do with the fact that I use the CodeIgniter framework, which does so much of the hard work for you. it’s easy to get complaisant.

I have been working with text files that contain multi-byte characters and had previously ensured that my database and tables were setup for UTF-8 and that everything in codeigniter was correctly configured. Yet still I was getting invalid character errors on the database insert.

As the text files were of varying formats, including excel’s unicode csv format, I had already ensured that the reading of the text file also included conversion to UTF-8. Thanks to the script on Practical Web Ltd, I was attempting to detect the format of the files and converting them to UTF-8 on the fly. Yet still I was getting invalid character errors on the database insert.

I even ran through my code line by line and checked for any string manipulation I was doing using non-safe string functions. Yet still I was getting invalid character errors on the database insert.

If I had any decent amount of hair left, I would certainly have pulled it all out by the time I figured out what was wrong. I only discovered the answer by accident when I decided to remove the string manipulation altogether. As soon as I did that, it worked a treat. Had I discovered a bug in the multibyte string functions? No.

I had not checked the default encoding of mbstring.

So please, make sure it is on your check list of things to do when dealing with multi-byte strings. Set up the default correctly or religiously use the encoding parameter in the multi-byte string functions.

Even better, you could use the great checklist on nicknettleton.com (see below), which seems to cover everything.

I totally deserved the dunce hat.

Edit: Looks like the link on nicknettleton.com is no longer available (thanks @Les). A little digging around led me to the same checklist on php UTF-8 on another site.

Revisited: Tcpdf – Variable Height Table Rows With MultiCell

A few weeks ago I wrote an article about creating variable height table rows using tcpdf. It was a neat solution and I liked it, which was the reason I blogged about it in the first place. However, it turns out that ‘sharing the love’ is not the only reason to blog about these things. It can also be a learning experience thanks to the commenting from you out there in the interwebs.

The particular comment in question, from mike, made me realise that there was a bit of a flaw in my neat idea – page breaks. Because I was first drawing the text, the automatic page breaks played havoc with the positioning when it came to drawing the borders. If the text was so long that it wrapped across enough lines to cause a page break, when I tried to reposition to draw the borders I was in completely the wrong place and it all went a little Pete Tong.

Still, not deterred by this little setback, I re-looked at the problem and have come up with a different approach that solves this problem and adds the ability to do a few more things.

When I relooked at the documentation, I discovered the useful little function: getNumLines(). This actually allows us to determine how many lines a string of text will take up, given a particular width. In effect, it allows us to do what I was using MultiCell to return, without actually drawing anything. This lets us to determined the maximum cell height with one line of code:

$linecount = max($pdf->getNumLines($row['cell1data'], 80),$pdf->getNumLines($row['cell2data'], 80),$pdf->getNumLines($row['cell3data'], 80));

Now that we know the cell height before we have drawn anything, we can use MultiCell() to draw the text and the borders at the same time.

This was not the end, however. My particular implementation was a grid of data that had borders between the column and not between the rows. All very pretty until that automatic page break happened and gave us a grid of data without a border at the bottom. To add insult to injury, the new page ended up with a grid of data with no border at the top!

To solve this, I needed to know if the row was going to be the first of the page, so I could draw the bottom border on the previous row and give the new row a top border. With the use of the page dimensions I could work this out:

$dimensions = $pdf->getPageDimensions();
$hasBorder = false; //flag for fringe case
 
foreach($data as $row) {
	$rowcount = 0;
 
	//work out the number of lines required
	$rowcount = max($pdf->getNumLines($row['cell1data'], 80),$pdf->getNumLines($row['cell2data'], 80),$pdf->getNumLines($row['cell3data'], 80));
 
	$startY = $pdf->GetY();
 
	if (($startY + $rowcount * 6) + $dimensions['bm'] > ($dimensions['hk'])) {
		//this row will cause a page break, draw the bottom border on previous row and give this a top border
		//we could force a page break and rewrite grid headings here
		if ($hasborder) {
			$hasborder = false;
		} else {
			$pdf->Cell(240,0,'','T'); //draw bottom border on previous row
			$pdf->Ln();
		}
		$borders = 'LTR';
	} elseif ((ceil($startY) + $rowcount * 6) + $dimensions['bm'] == floor($dimensions['hk'])) {
		//fringe case where this cell will just reach the page break
		//draw the cell with a bottom border as we cannot draw it otherwise
		$borders = 'LRB';	
		$hasborder = true; //stops the attempt to draw the bottom border on the next row
	} else {
		//normal cell
		$borders = 'LR';
	}
 
	//now draw it
	$pdf->MultiCell(80,$rowcount * 6,$row['cell1data'],$borders,'L',0,0);
	$pdf->MultiCell(80,$rowcount * 6,$row['cell2data'],$borders,'L',0,0);
	$pdf->MultiCell(80,$rowcount * 6,$row['cell3data'],$borders,'L',0,0);
 
	$pdf->Ln();
}
 
$pdf->Cell(240,0,'','T');  //last bottom border

Note that there is a fringe case that I came across. This was when the row would not cause a page break as it was exactly the height of the remaining space. Any attempt to draw the bottom border with a separate call would cause a page break. I solved this by ensuring in that particular case, the cell was drawn with the bottom border already.

What gives this method more power (apart from actually working) is that we know when there is going to be a new page and we could choose to force a new page and if we wanted. We could then re-plot the grid headings before creating the row. That way we can have the grid headings on every page.

I am determined to get a water-tight solution to this problem and I think this gets us closer. If you do find any problems with it, let me know and I will see if we can solve them. Thanks for your feedback.

Update: Bretton Eveleigh has written a class that encapsulates this and other helper methods into a format that makes creating tables with Tcpdf much easier. See the comments below for the link. Thanks Bretton.

Cross Browser Testing – Its All Wrong

It was not long ago that I got all excited when I came across the XenoCode virtualized browsers. This seemed a neat idea. At the time one could download an executable (they seem to only be available in-browser now) and run one of the major browsers without interfering with the operating system or confguration you had.

I was all for throwing away my virtual machines there and then. However, I soon realised that they were as lacking as all the other cross browser testing systems out there. The most recent of which is a product from Microsoft themselves. Called SuperPreview, it is actually closely linked with the Expression product and it allows you to display the same page in different version of Internet Explorer. This is a similar approach to IETester, which allows you to open tabs containing the different rendering engines. This is fine if you just want to see that IE6 renders things in a completely different manner to everything else out there but it does not help you one bit if you are actually trying to solve those rendering problems.

Why do none of the IE cross browser rendering tools contain the Developers Toolbar? I mean, it is not a fraction as useful as the wonderful Firebug but it is the nearest we have to an IE tool that actually assists the developer in ironing out those css problems. I have even resorted to the IECollection which claims to contain all the versions of Internet Explorer that have ever existed and to contain the developers toolbar but only manages to throw up error messages and still no sign of a working developer’s toolbar.

So here it is, a request to the producers of the current cross-browser products or to anyone who fancies the challenge. Create a tool that is actually useful. Make it contain all current browsers and some developers tools that allow real-time changing of CSS and whilst your there, make it highlight the padding and margins in that oh-so useful way that Firebug does. That way we can actually see what is causing the problem and try a few fixes without having to change the css file and refresh the page every time. Also, do a favour for the Mac users out there and make it cross-platform.

If it really was a useful tool, developers would buy it and most be willing to pay a substantial license fee. I spend an awful lot of the time waiting for virtual machines to boot and suffering the performance consequences of running them whilst developing, so I would be happy to fork out some cash to solve this problem.

Accurate Ems

Whilst supporting browsers that have no full-page zoom and keeping the accessibility at a maximum, the use of ems in your style sheet is a must. This is not a difficult thing to do and you can still think in terms of pixels, thanks to great tools like the Em Calculator.

A word of warning however. If you don’t specify your font-sizes with enough accuracy, it can lead to some strange problems. I came across this when creating a multi column list. I used the method where each column is created using a left and a negative top margin. The problem was that because I only used two decimal places for the sizes, my line-heights did not translate a whole number of pixels.

This meant that for each item in the list, the fraction compounded and the total height of each column was not what what I thought – I ended up with a slanted set of columns. This may have not been an issue except each of the browsers interpret the fraction of pixels in a different manner – some round up and some round down.

It turns out that using three decimal places for all Em sizes will solve this and ensure that fractional pixel sizes are eliminated. This way you get no size differences across the different browsers and more predictable layouts. You can adjust the number of decimal places with the Em calculator by clicking on the settings tab and adjusting the value there.

The Importance of a Good jQuery Selector

As a technical lead and working in a company that has had rapid expansion in the last six months or so, I have had the job of being a mentor to some of the new employees. I don’t think any of them had used jQuery before and so for every person that arrived, I needed to give a crash course on the subject. The jQuery selector is always one of the first things I describe and it is usually met with nods of understanding, as it is not a difficult concept to grasp when most developers are familiar with css and the DOM.

There have been two incidents recently however, that have made me realise that my crash course on jQuery selectors has been missing something vital. The first was when I found some time to explore the options of profiling some of our javascript code. When I first started this, the only option I could see was John Resig’s deep profiling script that injected the statistics at the bottom of the page. This had some limitations however, and I was very happy to open my news feeds soon after and find that he had added a couple of extra methods to FireUnit that enabled you to profile javascript function calls.

What I noticed straight away was that there were a lot of seemingly simple calls in our code that took a long time to execute. The reason for this was that the selectors were vague:

    $('.myclass').somefunction();

In order to find all the matches, even if there is only one, jQuery needs to traverse the entire DOM looking for any element that might have the matching class, and that takes time. By narrowing down the selector using, for instance, the type, jQuery can very quickly eliminate all other types. This speeds things up a lot.

The second incident was due to another ‘benefit’ of being a tech lead. I tend to get passed the most peculiar and hard to diagnose bugs. The bug in question was on a page that contained two autocomplete text boxes, one of which was part of a compound control. The bug manifested itself by not being able to select an item from the second autocomplete text box properly. I was handed the bug with the information that it seemed to happen only when you selected from the first autocomplete control first. Needless to say, there was a lot of poking around in the code of both the autocomplete and the compound control, which is particularly complex given the nature of the control.

Thanks to firebug‘s great console logging, I discovered that the variable that contained all the autocomplete list items seemed to be a combination of both controls. It was not too long before i discovered the code that caused the problem:

    listitems = $('.auto').children('li');

There was nothing mysterious going on at all, jQuery was asked to select all of list items in both controls, so it did.

What had happened in both of these incidents is that I had failed to mention the importance of an accurate selector in jQuery during my crash course. The new developers were blissfully unaware of the consequences of not specifying exactly which elements you want jQuery to select. What I had also failed to emphasise was another way of narrowing down and speeding up the jQuery selection – by use of the elements that you already have. In the case of the autocomplete, we already had a reference to the list itself, so it was simple:

   listitems = thelist.children('li');

This not only eliminated the cross-contamination of the two controls but seeded things up. So the rules to keep things fast and to save me headaches are simple:

  • Use a selector that very accurately matches the elements you require
  • Utilise references to elements you already have

Captcha Audio with ASP.NET

The subject of captcha images has been well covered. There are plenty of available resources for creating those warped letters. I created such a control that is a combination of quite a few different ideas and was quite proud of it until not thirty seconds after demonstrating it, someone asked whether it had a ‘play audio’ button for blind people. All of a sudden my fancy captcha control was not so fancy.

I tried to suggest using reCaptcha as a solution that had all the bells and whistles, but the customisation of my control trumped the somewhat fixed design of reCaptcha. So the problem of the audio captcha brewed a little in the back of my mind and a month or two later I turned back to it to see if I could find a solution.

A speech synthesis engine was not on the cards, so I figured that because the captcha image was a random collection of letters and numbers, the only way I could generate the appropriate audio was to have audio files of all the letters and numbers. I would then need to join them together, on demand, and play them from the web page.

Creating all the letters and numbers is straight forward enough (cue microphone and best-est speaking voice) and playing audio files from a web page has also become pretty easy thanks to the great SoundManager 2 javascript plugin.

The trickiest part was definitely joining mp3 files that SoundManager requires. MP3 audio files are particularly tricky as they can contain ID1 or ID3 tag information, so just joining them back to back would not create a correct MP3 file. What I needed to do was determine if a particular file had the tag information in it and strip it out if necessary. This took a lot of Googling and studying of the MP3 specification but I eventually managed to ensure the files had no ID2/3 tags in them before joining the files together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Imports System.IO
 
Public Class MP3Concatenator
 
    Public Shared Function Join(ByVal MP3sToJoin As Generic.List(Of String)) As MemoryStream
        Dim ms As New MemoryStream()
        Dim bw As New BinaryWriter(ms)
 
        'loop around each file and remove the tags and then concatenate the files
        For Each mp3File As String In MP3sToJoin
            Dim bytes() As Byte
            Dim fs As New FileStream(mp3File, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
            Dim br As New BinaryReader(fs)
            Dim audioStart As Integer = 0
 
            'Check for ID3 Tags
            fs.Position = 0
            If (System.Text.Encoding.ASCII.GetString(br.ReadBytes(3)).ToUpper = "ID3") Then
                'position of the header size bytes
                fs.Position = 6
                'MSB of size is Set to 0 and ignored so we need to convert the value
                audioStart = BitsToLow(br.ReadBytes(9))
                'add the header end position to this
                audioStart += 9
            End If
 
            'Check ID1 Tag
            fs.Seek(-128, SeekOrigin.End)
            If (System.Text.Encoding.ASCII.GetString(br.ReadBytes(3)).ToUpper = "TAG") Then
                'there is a ID3v1 tag on the end which needs removing
                fs.Position = audioStart
                bytes = br.ReadBytes(CInt(fs.Length - 128))
            Else
                fs.Position = audioStart
                bytes = br.ReadBytes(CInt(fs.Length))
            End If
 
            bw.Write(bytes)
            br.Close()
            fs.Close()
        Next
 
        ms.Position = 0
        Return ms
    End Function
 
    Private Shared Function BitsToLow(ByVal Size() As Byte) As Integer
        Dim Ret As Integer
        Ret = Size(3)
        If Size(2) <> 0 Then
            If CBool(Size(2) And 1) Then Ret += 128
            If CBool(Size(2) And 2) Then Ret += 256
            If CBool(Size(2) And 4) Then Ret += 512
            If CBool(Size(2) And 8) Then Ret += 1024
            If CBool(Size(2) And 16) Then Ret += 2048
            If CBool(Size(2) And 32) Then Ret += 4096
            If CBool(Size(2) And 64) Then Ret += 8192
        End If
        If Size(1) <> 0 Then
            If CBool(Size(1) And 1) Then Ret += 16384
            If CBool(Size(1) And 2) Then Ret += 32768
            If CBool(Size(1) And 4) Then Ret += 65536
            If CBool(Size(1) And 8) Then Ret += 131072
            If CBool(Size(1) And 16) Then Ret += 262144
            If CBool(Size(1) And 32) Then Ret += 524288
            If CBool(Size(1) And 64) Then Ret += 1048576
        End If
        If Size(0) <> 0 Then
            If CBool(Size(0) And 1) Then Ret += 2097152
            If CBool(Size(0) And 2) Then Ret += 4194304
            If CBool(Size(0) And 4) Then Ret += 8388608
            If CBool(Size(0) And 8) Then Ret += 16777216
            If CBool(Size(0) And 16) Then Ret += 33554432
            If CBool(Size(0) And 32) Then Ret += 67108864
            If CBool(Size(0) And 64) Then Ret += 134217728
        End If
        BitsToLow = Ret
    End Function
End Class

The trickiest part was handling the ID3 tag, as the specification states:

The ID3v2 tag size is encoded with four bytes where the most significant bit (bit 7) is set to zero in every byte, making a total of 28 bits. The zeroed bits are ignored..

Which is why I have the BitsToLow function in the code above.

The resulting concatenation is returned as a memory stream because I knew I could output this directly to the Response without writing the concatenated file to disk:

1
2
3
4
5
6
7
8
9
            ....
            Dim ms As IO.MemoryStream = Nothing
            ms = MP3Concatenator.Join(MP3FileList)
            Response.ContentType = "audio/mpeg"
            Response.ExpiresAbsolute = Date.MinValue
            If ms IsNot Nothing Then Response.OutputStream.Write(ms.GetBuffer, 0, CInt(ms.Length))
            ms.Close()
            Response.End()
            ....

By wiring up the CaptchaAudio.aspx page that returned the concatenated audio to the SoundManager plugin, I could create a link next to the captcha image, that played the letters in the image. Now my captcha control really was fancy.

Tcpdf – Variable Height Table Rows With MultiCell

This post as been superseded. Please see my new post on the subject.

During my use of the excellent tcpdf library for creating pdf documents with php, I came across an interesting problem when creating a pdf featuring a grid of data. Although I could have use an html grid to present the data, I was trying to avoid the use of html and keep to the Cell(), MultiCell(), Text() etc. methods to render the document.

The content of some of the cells in this particular grid were quite long and so some wrapping of text would (and needed to) occur. The MultiCell method handles this and will wrap the text, expanding accordingly. Now I was trying to make a grid row, so needed all of the cells to be the same height – and there lay the problem – I did not know how high to make the other cells. It was a chicken and egg situation: I needed to know the height of the cell before drawing them but did not know how high they needed to be until after I had drawn them.

I studied the documentation hard but realised that the answer was not to try and generate the cells, borders and all, in one pass. I could achieve what I needed by drawing each row twice – once for the content and once for the borders.

The MultiCell function kindly returns the height that the cell was drawn, so by remembering the maximum value it was when drawing a row of cells, I could then go back and draw the borders to that maximum height:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
foreach($data as $row) {
	$maxnocells = 0;
	$cellcount = 0;
	//write text first
	$startX = $pdf->GetX();
	$startY = $pdf->GetY();
	//draw cells and record maximum cellcount
	//cell height is 6 and width is 80
	$cellcount = $pdf->MultiCell(80,6,$row['cell1data'],0,'L',0,0);
	if ($cellcount > $maxnocells ) {$maxnocells = $cellcount;}
	$cellcount = $pdf->MultiCell(80,6,$row['cell2data'],0,'L',0,0);
	if ($cellcount > $maxnocells ) {$maxnocells = $cellcount;}
	$cellcount = $pdf->MultiCell(80,6,$row['cell3data'],0,'L',0,0);
	if ($cellcount > $maxnocells ) {$maxnocells = $cellcount;}
	$pdf->SetXY($startX,$startY);
 
	//now do borders and fill
	//cell height is 6 times the max number of cells
	$pdf->MultiCell(80,$maxnocells * 6,'','LR','L',0,0);
	$pdf->MultiCell(80,$maxnocells * 6,'','LR','L',0,0);
	$pdf->MultiCell(80,$maxnocells * 6,'','LR','L',0,0);
 
	$pdf->Ln();
}

Note that the last parameter I set in MultiCell is set to 0 which means that no new line occurs once a cell is drawn. This means that the cells will appear side-by-side and also means that I need to manually create the new line when I finish drawing the row.