Here's an unpopular / unloved topic: custom tags. I dunno if you're like me (poor you if you are ;-), but I spend most if my time these days writing the business logic parts of our app, and I certainly enjoy that part of my work more than the "presentation layer" stuff. I just find doing all the front-end stuff like the mark-up and styling it according to sometime else's idea of what looks good to be the tedious part of my work. I much prefer working on files that have a CFC file extension than CFM.
That said, one of the good things about ColdFusion is this whole mashing-up CFML and markup thing that ColdFusion makes so easy. However I think "ease of doing something" often leads to rubbish code: just because one can do something doesn't mean one should do it. Because it might not be a very good approach to things when one takes a broader view of things. To this end, I'm very keen on separating concerns into model, view and controller logic, whether it's done via a framework, or just a mindset and following some convention.
This article isn't about MVC, and I'm gonna ignore the M and C bits, and dwell on the V.
Whilst I love writing code, the one place I don't want to see code is in my views. I'm happy with a conditional or a loop, but if I find myself starting a
<cfscript>
block because I've got a series of lines of pure code coming, I consider I've messed-up. Note to any of my coworkers reading this: some of our coding guidelines prevent me from writing the code I'd prefer to, hence my output not necessarily reflecting my preferred approach here.Anyway, sometimes logic is either unavoidable or just more appropriate to be in a view than in a model. An example of this would be handling the striping on a table: that's def presentation logic, so home it in the presentation layer. However I still don't want to clutter the mark-up up with code peppered all through it. I'd really prefer my views to be about 95% mark-up.
This is where custom tags come in: I think they're often a much better fit for view layer code than method calls and inline logic. This article is not going to be about custom tags in general - if it would be helpful I can cover that in a separate article - it's just about looping with custom tags. Why I think this is noteworthy is that despite having done CF for over a decade, and having had a suspicion one can loop with a custom tag, I had no idea how to do it until reasonably recently. And then I came to be reading the docs about
<cfexit>
, and furrowed my brow at the "loop" option. So I decided I'd better work out what the story was.Firstly: the docs are rubbish. Whilst they mention the loop control stuff, they don't actually explain it. This is a perennial problem with ColdFusion's docs: they're not helpful. They state the obvious, and don't go onto explain themselves. In fact having just googled the CF10 docs site, I don't find a single match that explains the looping functionality of
<cfexit>
. That's useless.Anyway, here's some code that demonstrates how this works:
This code basically performs a number of iterations. Like a jerry-built version of
<cfloop>
. In and of itself it's not very useful, but it demonstrates the point.There's a few things going on here.
Looking briefly at
doLoop.cfm
, we see we're calling the loop.cfm
custom tag, and passing it two params: iterations
and index
. Pretty straight forward. Let's move on to loop.cfm
.Firstly, as with any custom tag we need to deal with two possible calls to the tag: as an opening tag, and as a closing tag; that's covered with the outer
<cfif>
statement, at lines 2, 8 & 16.At the top of the "start tag" block - lines 3-4 - we
param
our required attributes to make sure they were passed in. If they weren't we just let the code error. If this was production code I'd probably try
/catch
this and then throw
a specific exception that explains the necessity of the attributes, but that would just be clutter in this example.At line 6-7 we initialise our loop index variable, and then expose it to the calling code as the variable name passed in to do so. It's important when writing custom tags that interact with the calling code (via the
caller
scope) that we only do so via nominated variables, eg:
<cf_mytag variable="myVar" />
We then only mess with
caller.myVar
(well: caller[attributes.myVar]
within the tag code), because that's what the calling code is expecting. We don't want to be just hacking away at any old variable in the calling code's variables scope, because it might cause the custom tag to overwrite stuff it's not supposed to. Refactored code like custom tags or functions should never assume the way they will be called (eg: "well I know variables.foo
won't be being used in the calling code, so it's OK to mess with it in my custom tag": no, you know no such thing. It might be the case now, but it might not be the case in the future. Don't leave things to chance, and have uncontrolled code running about the place). A custom tag should only access what's been passed into it, and should minimise its interaction with the calling memory space to just variables specified by the calling code. That's a bit of a digression. Anyway, that's why we expose index via caller[attributes.index]
, rather than just setting caller.anyOldVariable
And that's all we do in the "start" block: initialise the variable and expose it. This means that on the line after the opening tag in
doLoop.cfm
, variables.i
is set to 1.If we now look at
doLoop.cfm
, we see we're just outputting the counter, and - just to be clichéd - "Hello World". At this juncture i=1
, so we get:
[1]Hello World
Next we encounter the closing tag, so we're back into the code for
loop.cfm
, this time we're in the <cfelse>
block (THISTAG.executionMode
equals "end" now).All the rest of the iterative code takes place in this block, so we're starting the next iteration of the loop here, so we increment the index.
And now we get to the meat of the example, the looping bit. Well first we check if we're still wanting to loop, and if we're done: exit. That'd be it for the custom tag, and the mainline code would continue on its merry way.
However here's the loopy bit: if we're still within the iteration count, we use
<cfexit method="loop">
to tell the custom tag to - basically - return to the top of the content of the custom tag call (ie: in doLoop.cfm
), and run it again. Then each time it gets to the closing tag, we repeat this until it's time to exit.Simple.
I reckon the
<cfexit>
approach to effecting this is a bit clumsy and not at all intuitive, but that's how it works. "Exit" should mean "exit" not "actually, I mean 'loop'". I have a similar opinion about how <cfoutput>
actually does looping as well as outputting. Oh well.So why do we want to do all this? You might think if you want to loop over stuff then just use
<cfloop>
. Well, sure, yeah, if that's all the logic you need to execute per iteration, then absolutely do that. However custom tags can hide a lot of logic from the view, so the view can just concentrate on the viewy stuff (for goodness sake, I'm beginning to sound like David Tennant... I'll not be saying "timey-wimey" though, because it was a crap line when he delivered it the first time, and was even more crap every other time he delivered it subsequently. And none of that will mean anything to anyone who does not watch Dr Who. Sorry).What say your view needs to loop over some data, but the data could be a recordset, or an array, or some XML or [whatever]. You don't want to have all the logic to work out how to loop over it cluttering up the view. I've written a wee custom tag that will render an HTML table for some "data" that is passed to it, and it will handle a recordset, array or XML. It's a bit of a daft example, but it's just to show the technique.
Here's the view:
There's only two CFML tags in there: a
<cfimport>
and a <cfoutput>
. It demonstrates loading a tag library via <cfimport>
which I think is a nicer way of dealing with custom tags than either <cf_mytag />
, or using <cfmodule>
and a full path to the file.Anyway, this code calls a custom tag called
table.cfm
three times: once with a query, once with an array, and once with some xml (q
, a
and x
respectively). But other than the source data, the syntax in the view is the same, and it's very simple. And it's not a million miles away from looking like HTML.FYI, the data I am using is thus:
Very simple stuff.
So now for the code for table.cfm. There's a bit more to this code:
That might look like a lot, but a lot of it is the handlers for the data types, not the actual looping bit.
We've got the same
if
/else
block controlling the execution mode, as well as param
ing our attributes. There's nothing more to say about that.Next we need to work a few things out about our data, namely how many rows it's got, and how to extract a row. Because of the different datatypes, the methods of performing this data extraction differs, so we need a different code block for each of recordset, array and XML. The helper functions that do this are at the bottom of the file.
Note one important thing here (this is nothing to do with custom tags, but a nice technique), but I'm doing this sort of thing:
getRow = getRowFromQuery; // note that's a function REFERENCE, not a function CALL
And getRowFromQuery is a function, thus:
struct function getRowFromQuery(query q, numeric i){
var row = {};
for (var col in listToArray(q.columnList)){
row[col] = q[col][i];
}
return row;
}
And there's another function
getRowFromArray()
and getRowFromXml()
. We need different functions because they need to act on different data, but we don't want to be repeating the whole if/elseif/else block each time we want to get a row. So we leverage the fact that a function is just a variable, so we can assign a function to another variable. Having done that, we can call getRow()
, and that'll just work for getting a row from a query, or the array, or some XML.Next we convert the headers into an array because arrays are more manageable than lists, and then we grab all the other attributes passed on the custom tag call - in this case
border
, cellpadding
and cellspacing
, and we just stick those on the table tag we end up outputting. It's really handy to be able to pass any attributes we want to the tag body... it makes it easier to make it blend in more with the HTML.So now we're ready to do something. We're still in the START block, which runs once at the beginning of the process, so we output our opening table tag, as well as a
<thead>
block with some <th>
tags, if the code has asked us to. Nothing interesting about that.We then, just before finishing the START block and returning to the calling code in the "body", need to make sure we set the variables the tag's body is expecting to find: the
#id#
, #english#
and #maori#
values. Because this process takes a few lines of code, and we need it twice, I've abstracted that out into a separate function, setCallerVars()
. That's the only reason that's a function. It didn't need to be. What the function does is first to get the current row from the data, and then loop over the columnNames
that were passed into the tag, and sets each of those column variables in the caller scope. So we end up with id, english and maori being set in test.cfm
, ready to be used.The START block now finishes, and processing returns to
test.cfm
, which outputs the <tr>
row: 1, One, Tahi.We're back into the custom tag's closing tag now, which is the END block within
table.cfm
, and this does the familiar thing of incrementing the row, checking if we've finished with all the rows (exiting if so), or outputting a row and looping back again.That's it. Easy.
And here's the output:
id | english | maori |
---|---|---|
1 | one | tahi |
2 | two | rua |
3 | three | toru |
4 | four | wha |
1 | tahi | one |
2 | rua | two |
3 | toru | three |
4 | wha | four |
tahi | one | 1 |
rua | two | 2 |
toru | three | 3 |
wha | four | 4 |
Very boring, but I think it's an OK demonstration of separating logic away from a view by using a custom tag, as well as demonstrating a "real world" use for a looping custom tag.
And that was a marathon effort to type, so now I'm gonna press "send", and pester y'all on Twitter, and probably go to bed. We've got a bloody long haul at work tomorrow...
Righto.
--
Adam