Saturday 3 May 2014

ColdFusion 11: what <cfclient> compiles down to

G'day:
One of Sean's comments on another article ("ColdFusion 11: <cfclient>... how does normal CFML code and <cfclient> code interact?") got me thinking:

This doesn't surprise me at all: cfclient generates JS that runs in the browser; outside cfclient, you have regular CFML that runs on the server. You can't expect them to know about each other. This is like the frequent RTFM questions on SO about using CFML variables in JS!


Whilst I don't think it's quite so painfully obvious as Sean would like us to think, he does raise a reasonably good point. I'll reproduce my response too (which is a reasonable prelude to this article):

I suppose it depends how the JS is generated. Is it extracted from the file at compile time and created? Or is it compiled in situ as part of the compiled class, then at runtime JS is generated from it and returned to the browser? I'll need to check that (I suspect it's done at runtime).

If it's done at compile time, then, yeah... there's no way initialising any variables referenced in the "server side" part of the source code which are also needed in the client side part of it, as the values won't be known.

If it's done at runtime, it should be easy enough though? Same as how one might do it with CFML writing out a <script> tag, containing JS variables which have CFML variable values as their values?

If it's all done at compile time, I don't see why <cfclient> is a *tag* as opposed to a different file type (.cfjs or something), as there's no merit in having both server-side (outwith the <cfclient> tags) and client side (within them) in the same file. Because never the twain shall meet, as you point out.

Looks like I have another blog article to write on the topic...

Cheers for the food for thought, Sean.
Let's back up a bit. Here's the code I am looking at:

<cfset message = "G'day World">
<cfclient>
    <cfoutput>#message#</cfoutput>
</cfclient>

As discussed in the article linked-to above is that this errors because the CF variable message is never recreated in the JS, so when <cfclient> writes out this JS:

globalDivStruct = null;
var _$junk_func = function() {
    var self = this;
    var variables = {};
    self.__init = function() {
        var localdivstruct = globalDivStruct;
        var __output_var = "";
        var tmpVarArray = {};
        localdivstruct.outputvar += message;
        return ""
    }
};

function __startPage__$junk() {
    document.write("\x3cdiv id\x3d'__cfclient_0'\x3e\x3c/div\x3e");
    window.ispgbuild = false;
    var clientDivStruct = {
        divId: "__cfclient_0",
        outputvar: ""
    };
    globalDivStruct = clientDivStruct;
    try {
        _$junk = new _$junk_func;
        _$junk.__init()
    } catch (__eArg) {
        if (__eArg !== "$$$cfclient_abort$$$") throw __eArg;
    }
    __$cf.__flush(clientDivStruct)
}
__startPage__$junk();

Then message has never been initialised, so I get a JS error. NB: all the references to "junk" in there are because the CFM file this code was compiled from was called junk.cfm.

What I was/am expecting to happen is the equivalent of this:

<cfset message = "G'day World">
<cfoutput>
<script>
    var message = "#message#";
    document.write(message);
</script>
</cfoutput>

Which yields this:

<script>
    var message = "G'day World";
    document.write(message);
</script>

Where <cfclient> takes care of working out that message needs to be carried over into the JS it generates; both as a JS variable with that name (although this doesn't matter), and - more importantly - with an initial value of whatever the ColdFusion variable message had when the <cfclient> code was executed. I do not think this is as far-fetched as Sean would have us think. For one thing: what's the bleedin' point of having other code in the file if it can't interact with the code within the <cfclient&gt; block?

Anyway, the question remained in my mind... how exactly does all this get compiled. Well junk.cfm gets compiled to this (well it gets compiled to bytecode; this is what the bytecode decompiles to):

import coldfusion.runtime.AttributeCollection;
import coldfusion.runtime.CFPage;
import coldfusion.runtime.CfJspPage;
import coldfusion.runtime.LocalScope;
import coldfusion.runtime.Variable;
import coldfusion.runtime.VariableScope;
import coldfusion.tagext.GenericTag;
import coldfusion.tagext.lang.ClientTag;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.Tag;

public final class cfjunk2ecfm37698129 extends CFPage {
    private Variable MESSAGE;
    static final Class class$coldfusion$tagext$lang$ClientTag;
    public static final Object metaData;

    static {
        class$coldfusion$tagext$lang$ClientTag = Class.forName("coldfusion.tagext.lang.ClientTag");
        metaData = new AttributeCollection(new Object[] {
            "Functions", new Object[0]
        });
    }

    public final void registerUDFs() {}

    protected final void bindPageVariables(VariableScope varscope, LocalScope locscope) {
        super.bindPageVariables(varscope, locscope);
        this.MESSAGE = bindPageVariable("MESSAGE", varscope, locscope);
    }

    public final Object getMetadata() {
        return metaData;
    }

    protected final Object runPage() {
        Throwable t8;
        Throwable t7;
        Object t6;
        int mode0;
        Object value;
        JspWriter out = this.pageContext.getOut();
        Tag parent = this.parent;
        bindImportPath("com.adobe.coldfusion.*");
        this.MESSAGE.set("G'day World");
        _whitespace(out, "\r\n");
        ClientTag client0 = (ClientTag) _initTag(class$coldfusion$tagext$lang$ClientTag, 0, parent);
        _setCurrentLineNo(2);
        client0.hasEndTag(true);
        try {
            if ((mode0 = client0.doStartTag()) != 0) client0.setJSCode("<script type=\"text/javascript\" src=\"/CFIDE/cfclient/cfclient_main.js\"></script>\n<script type=\"text/javascript\" src=\"/CFIDE/cfclient/cffunctions.js\"></script>\n<meta name=\"viewport\" content=\"width=device-width\">\n<script type='text/javascript'>globalDivStruct=null;var _$junk_func=function(){var self=this;var variables={};self.__init=function(){var localdivstruct=globalDivStruct;var __output_var=\"\";var tmpVarArray={};localdivstruct.outputvar+=message;return\"\"}};\nfunction __startPage__$junk(){document.write(\"\\x3cdiv id\\x3d'__cfclient_0'\\x3e\\x3c/div\\x3e\");window.ispgbuild=false;var clientDivStruct={divId:\"__cfclient_0\",outputvar:\"\"};globalDivStruct=clientDivStruct;try{_$junk=new _$junk_func;_$junk.__init()}catch(__eArg){if(__eArg!==\"$$$cfclient_abort$$$\")throw __eArg;}__$cf.__flush(clientDivStruct)}__startPage__$junk();\n</script>");
            if (client0.doEndTag() == 5) return null;
        } catch (Throwable localThrowable1) {
            client0.doCatch(localThrowable1);
        } catch (Throwable localThrowable2) {
            jsr 6;
            throw localThrowable2;
        }
        Object t9 = returnAddress;
        client0.doFinally();
        ret;
        return null;
    }
}

So this makes things tricky. Given the JS has already been written out, it's "too soon" to know about the runtime value of the CFML variable message to embed it into that JS string. I would have thought the writing out of the JS would be more clever than that, and do it the equivalent to my example above wherein the CFML variable is written out into the JS at runtime.

There needs, surely, to be some mechanism to pass variables - at runtime - over into the JS <cfclient> generates. Even if it's an attribute on the tag itself which "migrates" a struct of variables between the two would be something. But I'd like to think the CFML could be parsed more intelligently than that.

No doubt Sean will shortly advise as to the piece of the puzzle I am still missing..?

It's probably time for a beer though, I think.

--
Adam