As a baseline for some more research I am about to do, I wanted to get up to speed with how Railo implements CFC-based custom tags. I had read their blog articles about them:
- CFC-based Custom Tags by Example - Part 1
- CFC-based Custom Tags by Example - Part 2
- CFC-based Custom Tags by Example - Part 3
Intended Audience
Just before I begin... I am not going to compare each example with how one would implement it in a CFM-based tag. I am presuming that knowledge already. If my presumption is off, perhaps read the other articles I've tagged as "Custom Tags". That lot should cover everything to do with CFM-based custom tags. Now: CFC-based ones...Passing an attribute
The most basic implementation I could think of is this sort of thing:<!--- redden.cfm --->
<cfimport taglib="lib" prefix="t">
<t:redden message="Make this red">
This simply takes the attribute value and makes it red. And here's the implementation:
// Redden.cfc
component {
public boolean function onStartTag(required struct attributes, required struct caller){
echo('<span style="color:red">#attributes.message#</span>')
return true
}
}
Notes:
- Tag CFCs have an
onStartTag()
method. - This receives an argument containing all the tag's
attributes
, plus a reference to thecaller
scope. - The function returns a
boolean
(more on that in a tick).
Returning from onStartTag()
If one returnstrue
from onStartTag()
, processing continues through the rest of the tag set. However if one returns false
, processing skips to after the end of the closing tag:<!--- startTagReturnsFalse.cfm --->
<cfimport taglib="lib" prefix="t">
<t:startTagReturnsFalse return="true">
between tags<br>
</t:startTagReturnsFalse>
<hr>
<t:startTagReturnsFalse return="false">
between tags<br>
</t:startTagReturnsFalse>
<hr>
// StartTagReturnsFalse.cfc
component {
public boolean function onStartTag(required struct attributes, required struct caller){
echo("In start tag<br>")
return attributes.return ?: true
}
public boolean function onEndTag(required struct attributes, required struct caller, required string generatedContent){
echo(generatedContent)
echo("In end tag<br>")
return false
}
}
This example outputs this:
In start tag
between tags
In end tag
In start tag
Note that it's not just a case of not outputting "between tags": the content between the tags is not processed at all, not simply "not output". This could be handy if one was doing feature-toggling in views, or something.
Working with data between tag pairs
I kinda touched on this in the example above, but here's a dedicated example:<!--- bolden.cfm --->
<cfimport taglib="lib" prefix="t">
<t:bolden>
Make this bold
</t:bolden>
// Bolden.cfc
component {
public boolean function onEndTag(required struct attributes, required struct caller, required string generatedContent){
echo('<strong>#generatedContent#</strong>')
return false
}
}
Notes:
onEndTag()
also receives theattributes
andcaller
, but also thegeneratedContent
from between the tags- it returns a boolean too. On the whole one would want to return
false
here.
Looping
If we returnfalse
from onEndTag()
, processing continues after the end of the end tag. If we return true
however, processing loops back to the start tag.<!--- repeat.cfm --->
<cfimport taglib="lib" prefix="t">
<t:repeater times="5">
Make this repeat
</t:repeater>
// Repeater.cfc
component {
public boolean function onStartTag(required struct attributes, required struct caller){
variables.count = 1
return true
}
public boolean function onEndTag(required struct attributes, required struct caller, required string generatedContent){
echo("#arguments.generatedContent#<br>")
return ++count <= attributes.times
}
}
Here we return true until the internal counter is greater than
5
, at which point return false
and let processing continue.This is quite a nice way of handling this; much nicer than using
<cfexit>
to perform looping (something called "exit" should not "loop").Requiring an end tag
We can require an end tag easily enough:<!--- requiredEndTag.cfm --->
<cfimport taglib="lib" prefix="t">
<t:RequiredEndTag message="G'day world" />
<cftry>
<t:RequiredEndTag>
<cfcatch type="MissingEndTagException">
<cfdump var="#[cfcatch.type,cfcatch.message,cfcatch.detail]#">
</cfcatch>
</cftry>
// RequiredEndTag.cfc
component {
public RequiredEndTag function init(required boolean hasEndTag){
hasEndTag || throw(type="MissingEndTagException", message="missing end tag", detail="The requiredEndTag tag requires an end tag")
return this
}
public boolean function onStartTag(required struct attributes, required struct caller){
echo(attributes.message)
return true
}
}
Notes:
- We've added a new method here:
init()
. - It runs before anything else (unsurprisingly), and receives whether the tag has a closing tag.
- So we can check that, and if there isn't one: throw an exception
Error Handling
Railo also provides for an onError() method:<!--- onError.cfm --->
<cfimport taglib="lib" prefix="t">
<t:onError message="No error thrown">
<hr>
<t:onError throwInStartTag="true">
<hr>
<t:onError throwInEndTag="true">
<hr>
// OnError.cfc
component {
public boolean function onStartTag(required struct attributes, required struct caller){
attributes.throwInStartTag = attributes.throwInStartTag ?: false
!attributes.throwInStartTag || throw(type="StartTagException", message="Exception thrown in onStartTag()", detail="This tags demonstrates how exceptions are handled by onError()")
echo(attributes.message)
return true
}
public boolean function onEndTag(required struct attributes, required struct caller){
attributes.throwInEndTag = attributes.throwInEndTag ?: false
!attributes.throwInEndTag || throw(type="EndTagException", message="Exception thrown in onEndTag()", detail="This tags demonstrates how exceptions are handled by onError()")
return false
}
public boolean function onError(required struct cfcatch, required string source){
dump([cfcatch.type,cfcatch.message,cfcatch.detail])
return false
}
}
The output for this is as follows:
No error thrown
Array | |||
1 |
| ||
2 |
| ||
3 |
|
Array | |||
1 |
| ||
2 |
| ||
3 |
|
This demonstrates the first bug I have found in this stuff: an exception raised in
onEndTag()
is malformed.Update:
<panto-voice>Ooooh no it doesn't!</panto-voice>. As Micha points out below, the different error I'm getting is because of an actual error in my code! Still... read the rest of the section if you like... ;-)Let's look closer:
<!--- onErrorArgs.cfm --->
<cfimport taglib="lib" prefix="t">
<h3>Exception in init()</h3>
<t:onErrorArgs>
<hr>
<h3>Exception in start tag</h3>
<t:onErrorArgs throwInStartTag="true" />
<hr>
<h3>Exception in end tag</h3>
<t:onErrorArgs throwInEndTag="true" />
<hr>
// OnErrorArgs.cfc
component {
public OnErrorArgs function init(required boolean hasEndTag){
hasEndTag || throw(type="InitException", message="Exception thrown in init()", detail="This demonstrates the arguments onError() receives when an exception is thrown in #getFunctionCalledName()#()", extendedInfo=getFunctionCalledName(), errorCode=1)
return this
}
public boolean function onStartTag(required struct attributes, required struct caller){
attributes.throwInStartTag = attributes.throwInStartTag ?: false
!attributes.throwInStartTag || throw(type="StartTagException", message="Exception thrown in onStartTag()", detail="This demonstrates the arguments onError() receives when an exception is thrown in #getFunctionCalledName()#()", extendedInfo=getFunctionCalledName(), errorCode=2)
echo(attributes.message)
return true
}
public boolean function onEndTag(required struct attributes, required struct caller){
attributes.throwInEndTag = attributes.throwInEndTag ?: false
!attributes.throwInEndTag || throw(type="EndTagException", message="Exception thrown in onEndTag()", detail="This demonstrates the arguments onError() receives when an exception is thrown in #getFunctionCalledName()#()", extendedInfo=getFunctionCalledName(), errorCode=3)
return false
}
public boolean function onError(required struct cfcatch, required string source){
dump(arguments)
return false
}
}
Output:
Errors raised in
init()
and onStartTag()
work fine: all of type
, message
, detail
, errorCode
and extendedInfo
all get passed fine.However
onEndTag()
seems to mess this up: it doesn't pass any of it correctly. I'll raise a bug for this.Anyway, you get the idea:
onError()
catches errors occurring within the CFC. Note that if one returns true
from onError()
, the original exception is rethrown. If one returns false
: it's up to the code to deal with the error: processing will continue on its merry way after onError()
runs.Finally
(no, this is not the last bit...)Railo also has an
onFinally()
method, which is roughly analogous to the finally
in a try
/catch
/finally
construct.<!--- onFinally.cfm --->
<cfimport taglib="lib" prefix="t">
<h3>No exception</h3>
<t:onFinally />
<hr>
<h3>Exception in init()</h3>
<t:onFinally>
<hr>
<h3>Exception in start tag</h3>
<t:onFinally throwInStartTag="true" />
<hr>
<h3>Exception in end tag</h3>
<t:onFinally throwInEndTag="true" />
<hr>
<h3>Exception in onError</h3>
<t:onFinally throwInStartTag="true" throwInOnError="true" />
<hr>
// OnFinally.cfc
component {
public OnFinally function init(required boolean hasEndTag){
hasEndTag || throw(type="MissingEndTagException", message="missing end tag", detail="This demonstrates how onFinally() is called even after an exception in init()")
return this
}
public boolean function onStartTag(required struct attributes, required struct caller){
attributes.throwInStartTag = attributes.throwInStartTag ?: false
!attributes.throwInStartTag || throw(type="StartTagException", message="Exception thrown in onStartTag()", detail="This demonstrates how onFinally() is called even after an exception in onStartTag()", extendedInfo=attributes.throwInOnError?:false)
return true
}
public boolean function onEndTag(required struct attributes, required struct caller){
attributes.throwInEndTag = attributes.throwInEndTag ?: false
!attributes.throwInEndTag || throw(type="EndTagException", message="Exception thrown in onEndTag()", detail="This demonstrates how onFinally() is called even after an exception in onEndTag()")
return false
}
public boolean function onError(required struct cfcatch, required string source){
dump(var=[cfcatch.type,cfcatch.message,cfcatch.detail], label="Exception caught by onError()")
!(isBoolean(cfcatch.extendedInfo) && cfcatch.extendedInfo) || throw(type="OnErrorException", message="Exception thrown in onError()", detail="This demonstrates how onFinally() is called even after an exception in onError()")
return false
}
public void function onFinally(){
dump(var=arguments, label="onFinally() arguments")
}
}
Output:
I can't help but think
onFinally()
is a bit useless, as it doesn't get any arguments passed into it. I could see a case for it to everything all the other functions get passed into them. Otherwise it's very constrained as to what it can actually do. Same with onError()
really.Also I think Railo give away that they're not native English speakers here.
onFinally()
makes no sense. It should just be finally()
. When a function is prefixed with on
, it's generally considered an event handler, for the event that it's named after. Like a click handler is onClick()
or onRequestStart()
when the request starts. The event describes an action taking place. "Finally" is not an action, and it's not an event, so onFinally()
is wrong. Now that I think about it: onStartTag()
and onEndTag()
aren't great either: they are not event handlers really. I think doStartTag()
or runStartTag()
or processStartTag()
would be better. But those almost make sense: onFinally()
definitely does not.Sub tags
One can also implement sub tags. Here we enforce that our tag is being used within a parent tag:<!--- requiredToBeSubTag.cfm --->
<cfimport taglib="lib" prefix="t">
<t:RequiredToBeSubTag message="Called without BaseTag tag">
<hr>
<t:BaseTag>
<t:RequiredToBeSubTag message="called with BaseTag tag">
</t:BaseTag>
<hr>
// RequiredToBeSubTag.cfc
component {
public RequiredToBeSubTag function init(required boolean hasEndTag, parent){
(structKeyExists(arguments, "parent") && isInstanceOf(arguments.parent, "BaseTag")) || throw(type="InvalidTagNestingException", message="Context validation error for the RequiredToBeSubTag tag", detail="The tag must be nested inside a BaseTag tag")
return this
}
public boolean function onStartTag(required struct attributes, required struct caller){
echo(attributes.message)
return true
}
public void function onError(required struct cfcatch, required string source){
if (cfcatch.type != "InvalidTagNestingException"){
return true
}
dump([cfcatch.type,cfcatch.message,cfcatch.detail])
return false
}
}
When a tag is called as a sub tag, its
init()
method receives an additional argument: parent
. This contains the instance of the parent tag's CFC.One can enforce that a tag is being called as a particular tag's sub tag in two ways.
- The way I do it above.
- Simply define the parent argument as both required and of the specific type, eg:
init(required boolean hasEndTag, required BaseTag parent)
in this case.
I opted to do it "my way" because the exception raised when doing it the second way is very generic (it's just of type "expression", I think), so not easy to error-handle. I wish Railo and Adobe would put more thought into this sort of thing. Error handling of CFML errors is not a pleasure to effect.
Data interchange
Finally (yes, this is the last bit), one can interchange data between parent and child tags:<!--- page.cfm --->
<link rel="stylesheet" href="lib/css/styles.css">
<cfimport taglib="lib" prefix="doc">
<doc:page>
<p>
This is a test of the footnotes. This is the <doc:footnote>first footnote, and it does not have an href</doc:footnote>.
And now we have a second footnote: <doc:footnote title="specific title for second one">second footnote has a different title</doc:footnote>.
Continuing on we demonstrate giving the footnote an href to another doc: <doc:footnote href="someUrl">third footnote has an href</doc:footnote>,
and finally we've got one with both a <doc:footnote href="someUrl" title="this is the footnote title for the fourth footnote">separate title for the footnote text and has an href too</doc:footnote>.
That's enough testing.</p>
</doc:page>
// Page.cfc
component {
public Page function init(required boolean hasEndTag){
variables.footnoteCollection = []
return this
}
public boolean function onEndTag(required struct attributes, required struct caller, required string generatedContent){
if (!variables.footnoteCollection.len()){
echo(generatedContent)
return false
}
var footnotes = variables.footnoteCollection.reduce(function(reduction, footnote, i){
reduction &= '<li id="footNote#i#">'
var textToDisplay = footnote.title
if (footnote.keyExists("href")){
textToDisplay = '<a href="#footnote.href#">#textToDisplay#</a>'
}
reduction &= (textToDisplay & "</li>")
return reduction
}, "")
echo(generatedContent &"<hr><ol>" & footnotes & "</ol>")
return false
}
public numeric function addFootnote(required struct footnote){
footnoteCollection.append(footnote)
return footnoteCollection.len()
}
}
// Footnote.cfc
component {
public Footnote function init(required boolean hasEndTag, required Page parent){
variables.parent = parent
return this
}
public boolean function onEndTag(required struct attributes, required struct caller, required string generatedContent){
var thisFootnote = {}
if (attributes.keyExists("href")){
thisFootnote.href = attributes.href
}
thisFootnote.title = attributes.title ?: generatedContent
var thisFootnoteIndex = variables.parent.addFootnote(thisFootnote)
echo('<a href="##footNote#thisFootnoteIndex#" class="footnoteLink"><span class="footnote">#generatedContent#</span> <sup>#thisFootnoteIndex#</sup></a>')
return false
}
}
I've used a more real-world example here. We have got a page and a footnote tag. And this allows one to write a page with embedded footnotes, and then the notes are presented nicely at the bottom of the page:
That's at least a bit cool, yes?
Let's have a look at some of the code again. This is from
Page.cfc
:public Page function init(required boolean hasEndTag){
variables.footnoteCollection = []
return this
}
Here we set up an array to hold our footnotes...
public numeric function addFootnote(required struct footnote){
footnoteCollection.append(footnote)
return footnoteCollection.len()
}
... and provide a function to append a footnote to that array.
Now in
Footnote.cfc
we do this:public Footnote function init(required boolean hasEndTag, required Page parent){
variables.parent = parent
return this
}
In the
init()
method we put the passed-in parent into the variables scope for later...
var thisFootnoteIndex = variables.parent.addFootnote(thisFootnote)
And in
onEndTag()
in Footnote.cfc
we append to the parent's footnote collection. This shows two-way data interchange with the parent, as the count of the footnotes is returned too.The other bit of interesting code in
Page.cfc
has nothing to do with custom tags, but is an example of array reduction at work:var footnotes = variables.footnoteCollection.reduce(function(reduction, footnote, i){
reduction &= '<li id="footNote#i#">'
var textToDisplay = footnote.title
if (footnote.keyExists("href")){
textToDisplay = '<a href="#footnote.href#">#textToDisplay#</a>'
}
reduction &= (textToDisplay & "</li>")
return reduction
}, "")
Here we use
reduce()
to reduce an array to a string, where the string is a series of <li>
elements. Cool! One can do some nice stuff in CFML these days!Conclusion
I like the way Railo has implemented custom tags. It's got some blemishes and a coupla bugs, but on the whole it's a thoughtful implementation.Have any of you used 'em much? Tempted to now?
NB, I've raised some tickets with Railo:
Exceptions from onEndTag not passed correctly- onFinally() should just be finally()
- Improve onFinally()
- Cannot default first argument in reduce()'s callback
--
Adam