Saturday 2 August 2014

Railo: CFC-based custom tags

G'day:
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:
But it hadn't completely "sunk in" for me just by reading. I decided to work from top to bottom of the technology, demonstrating to myself all the various facets of custom tags and how they are implemented via a CFC. And here it all is...

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 the caller scope.
  • The function returns a boolean (more on that in a tick).

Returning from onStartTag()

If one returns true 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 the attributes and caller, but also the generatedContent from between the tags
  • it returns a boolean too. On the whole one would want to return false here.

Looping

If we return false 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
stringStartTagException
2
stringException thrown in onStartTag()
3
stringThis tags demonstrates how exceptions are handled by onError()

Array
1
stringexpression
2
stringkey [MESSAGE] doesn't exist
3
string

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.

  1. The way I do it above.
  2. 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>&nbsp;<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:

--
Adam