Login Register

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