Advanced ContentPane Usage

Introduction

A common use case for DHTML/ajax is to fetch a fragment of html using XHR or some other way, and change the innerHTML of a div with that content. Problem with this is that it doesn't instanciate widgets and doesn't fire scripts. ContentPane was created to make widgets and scripts work and reduce the potential for memory leaks. ContentPane is a base widget for many (Html)widgets, it handles remote loading as well as local setting of content and instanciating widgets in that content. Think of it as islands in your page that can easily switch content using setContent() or setUrl().

Many other widgets inherits ContentPane, like Tooltip, Dialog, FloatingPane etc. That means that all the methods and properties of ContentPane also applies to them.

ContentPane is often used as children of Layout widgets like LayoutContainer, TabContainer, AccordionContainer

Dont misstake it for a Iframe though, It should not be used on very large html fragments.

Usage

Simple usage ... <div id="cpane" dojoType="contentPane" href="initialContent.html"><div> <a href="javascript:dojo.widget.byId('nextContent.html')">Goto nextPage</a> ...

Basic options

  • loadingMessage Default: "Loading..." Set a custom loading message, see onDownloadStart to avoid showing this message completely
  • adjustPaths Default: true When content is setUrl'ed from a different folder paths to images, links etc. is adjusted so the point to the correct dir
  • href Default: "" Use this to grab initial content when contentpane is created.
  • extractContent Default: true Only insert the html that is inside Script and style tags are not affected by this setting
  • parseContent Default: true Create widgets inside content
  • cacheContent Default: true Use dojo.io.bind javascript cache and, if it exists, browsers cache
  • preload Default: false Lazyload switch, if true it will download content even if domNode is hidden Note: To make use of the default lazyload setting you need to hide your domNode Like this: <div dojoType="Dialog" style="display:none;"></div>
  • bindArgs Default: {} Send in a custom setting to the dojo.io.bind call, like: mypane.bindArgs = {sync: true, preventCache: false}; mypane.seetUrl('nextHtmlFragment.html');
  • refreshOnShow Default: false Re-download content each time ContentPane is shown again
  • executeScripts Default: false Fire scripts in content Note: see scriptSeparation
  • scriptSeparation Default: true Run scripts in a separete scope for ContentPane Note: set to false if you want similar behaviour as a normal pageload
  • handler Default: "" Java function name, generate pane content
  • isLoaded Default: false Tells wheather we are loaded or not, see also: onLoad() and addOnLoad()

Methods apart form those provided by ContentPane's superclass HtmlWidget

  • setContent(String or DomNode) Use this instead of innerHTML
  • setUrl(String or dojo.uri.Uri) Use this to set a new href and download and diplay that href
  • refresh() Re-download and display href
  • loadContents() Like refresh but only when isLoaded is false
  • setHandler(Function) Set a function callback for javacontent generation
  • abort() Abort a async download
  • addOnLoad(Object, "functionname" or Function) Push a callback that will be run when content the next onLoad occurs. It's a fire and forget stack, if you want a callback each onLoad, see onLoad() Works for setContent as well
  • addOnUnload(Object, "functionname" or Function) Same as addOnLoad but for onUnLoad event

Methods (Intended as event hooks using dojo.event.connect)

  • onLoad() Called when everything rendered initialized and ready
  • onUnload() Called before content is cleared
  • onDownloadStart(e) preventDefault'able Called before a download occurs To prevent showing the loading message, do like this:
  • onDownloadEnd(url, string) Called when download is completed, before it is setContent'ed
  • onDownloadError(e) preventDefault'able Called when a load error occures, before The load error message is displayed. Prevent it the same way as onDownloadEnd Tip: During debug, you can display debug info like responseHeaders, responseText etc.
  • onContentError(e) preventDefault'able Called when content insertion generates a error, before error mesage is displayed, like DOM faults, dojo.require() *syntax* faults etc.
  • onExecError(e) Called when there is errors evaling script, doing java setContent and download errors of external scripts
  • In order to prevent the default messages you can do something like this:

    <script>
    	var myLoadMessage = {
    		show: function(event){
    			event.preventDefault();
    			... custom code here
    		}, 
    		hide: function(){...}
    	}
    
    	dojo.addOnLoad(function(){
    		var pane = dojo.widget.byId('myPaneId');
    			dojo.event.connect(pane, "onDownloadStart", myLoadmessage, "show");
    	});
    </script>
    <div dojoType="ContentPane" id="myPaneId">...startcontent...</div>
    
    or
    
    <div dojoType="ContentPane" 
    	onDownloadStart="myLoadMessage.show(arguments[0]);">...startcontent...</div>
    		

When used as a child to TabContainer, AccordionContainer or PageContainer TabContainer, AccordionContainer or PageContainer extends Widgets with these extra options

  • label Tab text
  • selected Preselect this tab after creation
  • closable Display close button
  • Note:
  • Height and width settings is done on the Container, not the ContentPane.
  • In order for lazyload to work you have to hide your domNode initialy

When used as a child of LayoutContainer LayoutContainer package extends Widgets with this option

  • layoutAlign "left", "right", "bottom", "top", or "client" see layout section of the book for more info

FAQ

  • Why doesn't my widgets show up? Most likely you have used mypane.domNode.innerHTML = htmlstr; Use setContent instead: mypane.setContent(htmlstr);
  • Why doesn't lazy load work? You have to hide your domNode, style="display:none;", initialy while creating ContentPane
  • ContentPane displays strange looking characters when loaded remotly in some browsers, why? Like all server communication your browser need to know what charset your html is encoded with. Make sure your server is sending the correct Content-type header. example in php: header("Content-type: text/html; charset=utf-8"); Make sure you type utf-8 and not utf8, else IE will generate a warning and bail out.
  • Why is ContentPane so slow? You probaly send it a big chunk of html with deeply nested tags.
    • Send it a html fragment, not a complete page with doctype and everything
    • Try to make the HTML simpler and use css for styling
    • Turn off the options you dont need: adjustPaths, extractContent, executeScripts, parseContent
    • Consider redesigning your page with serveral ContentPanes which grabs a smaller portion of your html that way you dont have to scan, render and create as many DomNodes/Widgets on each update.
    • Perhaps dojo.io.updateNode("nodeId", "myUrl") is all you need, and ContentPane is to heavy for your needs
  • dojo.addOnLoad() is called to early, before my Content is loaded See onLoad event and addOnLoad for ContentPane, it is usualy easier to do this using <script>_container_.addOnLoad(..)</script> in your downloaded page, just be sure to set executeScripts=true
  • My inline scripts doesnt work when loaded in ContentPane, I have turned executeScripts to true? Short answer: set scriptSeparation=false Long answer: ContentPane separates scope of scripts between different ContentPane's see: scriptScope page in dojo book
  • When I press submit in my form inside a ContentPane, the whole page unloads, why? ContentPane doesn't have a form handling feature, look at dojo.widget.Form or see sample use case below
ContentPane examples

    scriptScope

    Executing scripts in ContentPane in dojo-0.3.1





    This is a explanation about scripthandling in a ContentPane in other words when you set executeScripts to true, it is false by default.


    ContentPane has some convenience functions related to scripts that makes life easier:



    • .addOnLoad()
    • .addOnUnLoad()
    • And the replacement for scriptScope to dojo.widget.byId('thisWidgetId').scriptScope in html content attributes.


    All scripts within content is evaled in the ContentPane property scriptScope, this means that you can have 2 or more content panes in the same page with the same name without risk of collision. This implementation does have its pros and cons. but correctly handled they will be useful. Also the scripts evals before widgets is parsed and after html is inserted.



    So the content scripts is evaled inside a freestanding scope that inherits window. Take this function declaration.



    <script>
        var i = 0;
        function addToI( j ){
            i = i + j;
            return i;
        }
    </script>
    
    becomes (from window scope):
    
    (function( ){
        var i = 0;
        function addtoI( j ){
            i = i + j;
            return i;
        }
    })
    


    Due to the way javascript works addToI will be private and you cant reach it. Read Douglas Crockford excellent description of this. http://javascript.crockford.com/private.html



    You can fix this in a couple of ways, one is to append it to global



    <script>
        i = 0; // note lack of var
        addToI = function( j ){ // we could also do window.addToI = function( j ){ ...
            i = i + j;
            return i;
        }
    </script>
    
    becomes (from window scope)
    
    i = 0;
    function addtoI( j ){
        i = i + j;
        return i;
    }
    


    This is'nt recommended, if you want global scripts It is better to include them in the main page the old fashion way.



    A better way would be to use Privileged functions:



    <script>
        this.i = 0;
        this.addToI = function( j ){
            this.i = this.i + j;
            return this.i;
        }
    </script>
    
    becomes (from window scope)
    
    (function(){
         this.i = 0; // now it is a property of this function and can be reached from the outside.
         this.addToI = function( j ){
            this.i = this.i + j;
            return this.i;
         }
    })
    


    As you might have guessed by now the (function(){...}) is the function scope that is reference held by ContentPane.scriptScope.

    So to call the addToI function from the outside we can do:

    var added = dojo.widget.byId('myPaneId').scriptScope.addToI( 10 );
    dojo.debug(added) // prints 10
    
    added = dojo.widget.byId('myPaneId').scriptScope.addToI( 10 );
    dojo.debug(added); // prints 20
    
    and so on...
    
    Now lets say we have html that would like to alert the value of i (this.i) in plain html that would be:
    <button onclick="alert(i);">Tell me i !</button> 
    // As explained above this wont work in ContentPane(unless you set it to global by omitting var).
    
    Now if we now the ID of the contentPane that pulls in this content we could do:
    <button onclick="alert(dojo.widget.byId('myPaneId').scriptScope.i);">Tell me i !</button>
    // this will work in ContentPane
    
    That is'nt very useful as we don't always know the ID of the contentPane that pulls in the html when we write the content.



    ContentPane has a convenience replacer function the scans the html and replaces all occurrences of the keyword scriptScope in html attributes. So to achieve the above:
    <button onclick=" scriptScope.i">Tell me i !</button>
    // this will work in ContentPane
    // NOTE: Due to a bug in ContentPane 0.3.1 you need to add a extra space before the keyword
    // Thank you Sasha Firsov for finding that! 
    
    The parent scope of scriptScope is window, the reason for that is to avoid messing with widget internals. Just imagine the disaster a redefinition of setUrl function would cause otherwise.



    To enable the content scripts to talk to the containing ContentPane there is set a private variable on scriptScope construction.

    That is the _container_ variable. Lets say we have a widget in our content that we would like to connect a event callback on:
    ... some content
    <div dojoType="DatePicker" id="myPicker"></div>
    ... rest of content
    
    A content script could look like:
    <script>
        var o = {
            storeDate: function( ){
                var datePick = dojo.widget.byId('myPicker');
                var date = datePick.storedDate;
                // save date somewhere
            }
        };
        _container_.addOnLoad(function(){
            var picker = dojo.widget.byId('myPicker');
            dojo.event.connect(picker, "onSetDate", o, "storeDate");
        });
    
        // remember to disconnect onUnLoad, very important!!
        _container_.addOnUnLoad(function(){
            var picker = dojo.widget.byId('myPicker');
            dojo.event.disconnect(picker, "onSetDate", o, "storeDate");
        });
    </script>
    


    When the content is cleared in ContentPane the scriptScope is unreferenced, this means that if there are no other variables that holds reference to any of the scriptScope objects (like event connects, varible connection etc), the script will be garbage collected by the javascript engine (memory freed).



    Different browsers are more or less conservative about GC (garbage collect), IE and Mozilla beeing the more relaxed browsers, khtml and Presto (Opera) engines are more conservative.



    Some usecase samples

    Lets say you have page that you need to run in as both stand alone and within a ContentPane.

    Then you can do something similar to this.

    Courtesy of Sasha Firsov for a pointer to this example!
    <html>
    <head>
    <script>
        var djConfig = {isDebug: true};
    </script>
    <script src="dojo/dojo.js"></script>
    <script>
        var scriptScope = this;
        if(typeof _container_ == 'undefined'){
            var _container_ = dojo;
        }
    
        _container_.addOnLoad(function(){
            dojo.debug("Successfully loaded!");
        });
    
        this.doWhenClicked = function(txt){
            dojo.debug(txt);
        }
    </script>
    <body>
        <a href="javascript:scriptScope.doWhenClicked('You clicked a link!');">Click here!</a>;
    </body>
    </html>
    




    Perhaps you want to prevent all <a href='...' link clicks with a ContentPane from clearing your page, and use the href to set your client ContentPane.
    *********Your mainpage*********** 
    <html>
    <head>
    <script src="dojo/dojo.js"></script>
    <script>
        dojo.require("dojo.widget.ContentPane");
        dojo.require("dojo.widget.LayoutContainer");
    
        function changeUrlInClient(url){
            var client = dojo.widget.byId("client");
            client.setUrl(url);
        }
    </script>
    </head>
    <body>
        <div dojoType="LayoutContainer" layoutChildPriority='none' style="border: 1px solid blue; width: 800px; height: 300px;">
            <div dojoType="ContentPane" layoutAlign="left" style="width: 200px;" executeScripts="true" href="linkpage.html"></div>
            <div widgetId="client" dojoType="ContentPane" layoutAlign="client" style="border:1px solid red;"></div>
        </div>
    </body>
    </html>
    
    *******linkpage.html************
    <html>
    <head>
    <script>
        var o = {
            listen: function(evt){
                // if the onclick came from a 
    </head>
    <body>
        <a href="content1.html">content1</a>
    <a href="content2.html">content2</a>
    <a href="content3.html">content3</a>
    <a href="content4.html">content4</a> </body> </html>




    A simple example of using a form in ContentPane. NOTE! dont use this login example in real world applications, password is sent in cleartext
    **********mainpage************
    <html>
    <head>
    <script src="dojo/dojo.js"></script>
    <script>
        dojo.require("dojo.widget.FloatingPane");
        dojo.require("dojo.widget.Button");
    </script>
    </head>
    <body>
        <div dojoType="FloatingPane"
            title="Login example"
            style="width: 300px; height: 300px;"
            executeScripts="true"
            cacheContent="false"
            href="login.php">
        </div>
    </body>
    </html>
    
    *********login.php**********
    <?php
        session_start();
    
        // are we trying to login?
        if(isset($_GET["login"])){
            // this could of be a database instead
            $users = array(
                    "JohnDoe"=>
                        array("pass"=>"foo", "id"=>1),
                    "JaneDoe"=>
                        array("pass"=>"bar", "id"=>2),
                    "JuniorDoe"=>
                        array("pass"=>"baz", "id"=>3)
                    );
    
            if(isset($_POST["user"]) && isset($_POST["pass"])){
                $pass = $_POST["pass"];
                $user = $_POST["user"];
                if(isset($users[$user]) && ($users[$user]["pass"] == $pass)){
                    $_SESSION["id"] = $users[$user]["id"];
                    exit("(true);");
                }
            }
    
            //if we get here we have failed to login
            exit("(false);");
        }
    
        // logout?
        if(isset($_GET["logout"])){
            unset($_SESSION["id"]);
        }
    
        if(isset($_SESSION["id"])){
            // it is safe to show secret content
    ?>
    
        <script type="text/javascript">
            this.logout = function(){
                _container_.setUrl("login.php?logout=true");
            }
        </script>
        <h3>You have successfully logged in!</h3>
        showing secret content here
    <a href="#" onclick="scriptScope.logout();">log out</a> <?php }else{ //no it wasnt safe, show our login script ?> <script type="text/javascript"> this.ok = function(){ _container_.domNode.style.cursor = "wait"; dojo.io.bind({ formNode: dojo.byId("login"), mimetype: "text/javascript", handler: function(type, data){dojo.debug(data); _container_.domNode.style.cursor = ""; if(type=="load"){ if(data){ _container_.setUrl("login.php"); }else{ dojo.byId("message").innerHTML = "Wrong username or password"; } }else{ dojo.byId("message").innerHTML = "An error occured while login, please try again."; } } }); } this.quit = function(){ _container_.hide(); } </script> <form name="login" id="login" method="post" action="login.php?login=true"> <div id="message" style="text-align:center; color: red;">You need to login</div> <label for="user">Username: <input type="text" name="user"/>
    <label for="pass">Password:</label> <input type="password" name="pass"/> <button dojoType="Button" onClick="scriptScope.ok();"/>login</button> <button dojoType="Button" onClick="scriptScope.quit();">quit</button> </form> <?php } ?>




    In the follwing scenario you wont need executeScripts.

    In fact it wont affect the script at all, the script will run just as any other regular script in ordinary page
    <html>
    <head>
        <script src="dojo/dojo.js"></script>
        <script>
            dojo.require("dojo.widget.ContentPane");
        </script>
    </head>
    <body>
        <div dojoType="ContentPane" >
            <script>
                // this script will fire before dojo makes our parent a ContentPane widget, so it wont be 
                // affected by executeScripts at all.
                // i wont have a _container_ variable and scriptScope wont hide any variables
                // it will work just as a inlne javascript block always has
    
                alert("This alert will fire event if you have executeScripts=false");
            </script>
        </div>
    </body>
    </html>
    






    It should be fairly easy to pull in some small customized scripts that is tweaked to the content, like a form validation script or a button callback.

    The executeScripts and scriptScope has the potential to be very usefull, I cant think of all the possible implementations but probably you can.



    Catches:



    • If you event.connect to _container_ be sure to disconnect onUnLoad, else you get all sorts of strange errors
    • Be sure to unref. all references into and out of scriptScope before setting new content, else there will be a memleak