diff options
33 files changed, 1458 insertions, 471 deletions
diff --git a/.gitattributes b/.gitattributes index 102eddc9..3ccd211a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -709,6 +709,21 @@ demos/blog/themes/Fall/style.css -text demos/blog/themes/Spring/style.css -text demos/blog/themes/Summer/style.css -text demos/blog/themes/Winter/style.css -text +demos/chat/index.php -text +demos/chat/protected/.htaccess -text +demos/chat/protected/App_Code/ChatBufferRecord.php -text +demos/chat/protected/App_Code/ChatUserManager.php -text +demos/chat/protected/App_Code/ChatUserRecord.php -text +demos/chat/protected/App_Code/chat.db -text +demos/chat/protected/application.xml -text +demos/chat/protected/pages/Home.page -text +demos/chat/protected/pages/Home.php -text +demos/chat/protected/pages/Login.page -text +demos/chat/protected/pages/Login.php -text +demos/chat/protected/pages/config.xml -text +demos/chat/protected/pages/send.gif -text +demos/chat/protected/pages/send.png -text +demos/chat/protected/pages/style.css -text demos/composer/index.php -text demos/composer/index2.php -text demos/composer/protected/.htaccess -text @@ -1116,7 +1131,10 @@ demos/quickstart/protected/pages/GettingStarted/sequence.gif -text demos/quickstart/protected/pages/GettingStarted/sequence.vsd -text demos/quickstart/protected/pages/Search.page -text demos/quickstart/protected/pages/Search.php -text +demos/quickstart/protected/pages/Tutorial/AjaxChat.page -text demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page -text +demos/quickstart/protected/pages/Tutorial/chat1.png -text +demos/quickstart/protected/pages/Tutorial/chat2.png -text demos/quickstart/protected/pages/Tutorial/example1.png -text demos/quickstart/protected/pages/Tutorial/example2.png -text demos/quickstart/protected/pages/ViewSource.page -text @@ -2043,6 +2061,8 @@ tests/FunctionalTests/active-controls/protected/pages/ActiveRedirectionTest.page tests/FunctionalTests/active-controls/protected/pages/ActiveRedirectionTest.php -text tests/FunctionalTests/active-controls/protected/pages/CallbackCustomValidatorTest.page -text tests/FunctionalTests/active-controls/protected/pages/CallbackCustomValidatorTest.php -text +tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page -text +tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateComponent.php -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateComponent.tpl -text tests/FunctionalTests/active-controls/protected/pages/CustomTemplateControlTest.page -text @@ -2077,6 +2097,8 @@ tests/FunctionalTests/active-controls/protected/pages/PopulateActiveList.page -t tests/FunctionalTests/active-controls/protected/pages/PopulateActiveList.php -text tests/FunctionalTests/active-controls/protected/pages/PostLoadingTest.page -text tests/FunctionalTests/active-controls/protected/pages/PostLoadingTest.php -text +tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page -text +tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php -text tests/FunctionalTests/active-controls/protected/pages/RatingList.page -text tests/FunctionalTests/active-controls/protected/pages/RatingList.php -text tests/FunctionalTests/active-controls/protected/pages/RepeaterWithActiveControls.page -text diff --git a/demos/chat/index.php b/demos/chat/index.php new file mode 100644 index 00000000..17ed21d0 --- /dev/null +++ b/demos/chat/index.php @@ -0,0 +1,29 @@ +<?php
+
+$frameworkPath='../../framework/prado.php';
+
+/** The directory checks may be removed if performance is required **/
+$basePath=dirname(__FILE__);
+$assetsPath=$basePath."/assets";
+$runtimePath=$basePath."/protected/runtime";
+
+$sqliteDbDir = $basePath."/protected/App_Code";
+$sqliteDb = $sqliteDbDir."/chat.db";
+
+if(!is_file($frameworkPath))
+ die("Unable to find prado framework path $frameworkPath.");
+if(!is_writable($assetsPath))
+ die("Please make sure that the directory $assetsPath is writable by Web server process.");
+if(!is_writable($runtimePath))
+ die("Please make sure that the directory $runtimePath is writable by Web server process.");
+if(!is_writable($sqliteDbDir))
+ die("Please make sure that the directory $sqliteDbDir is writable by Web server process.");
+if(!is_writable($sqliteDb))
+ die("Please make sure that the sqlite file $sqliteDb is writable by Web server process.");
+
+require_once($frameworkPath);
+
+$application=new TApplication;
+$application->run();
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/.htaccess b/demos/chat/protected/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/demos/chat/protected/.htaccess @@ -0,0 +1 @@ +deny from all
\ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatBufferRecord.php b/demos/chat/protected/App_Code/ChatBufferRecord.php new file mode 100644 index 00000000..cf3c651f --- /dev/null +++ b/demos/chat/protected/App_Code/ChatBufferRecord.php @@ -0,0 +1,64 @@ +<?php
+
+class ChatBufferRecord extends TActiveRecord
+{
+ public $id;
+ public $for_user;
+ public $from_user;
+ public $message;
+ private $_created_on;
+
+ public static $_tablename='chat_buffer';
+
+ public function getCreated_On()
+ {
+ if($this->_created_on === null)
+ $this->_created_on = time();
+ return $this->_created_on;
+ }
+
+ public function setCreated_On($value)
+ {
+ $this->_created_on = $value;
+ }
+
+ public static function finder()
+ {
+ return parent::getRecordFinder('ChatBufferRecord');
+ }
+
+ public function saveMessage()
+ {
+ foreach(ChatUserRecord::finder()->findAll() as $user)
+ {
+ $message = new self;
+ $message->for_user = $user->username;
+ $message->from_user = $this->from_user;
+ $message->message = $this->message;
+ $message->save();
+ if($user->username == $this->from_user)
+ {
+ $user->last_activity = time(); //update the last activity;
+ $user->save();
+ }
+ }
+ }
+
+ public function getUserMessages($user)
+ {
+ $content = '';
+ foreach($this->findAll('for_user = ?', $user) as $message)
+ $content .= $this->formatMessage($message);
+ $this->deleteAll('for_user = ? OR created_on < ?', $user, time() - 300); //5 min inactivity
+ return $content;
+ }
+
+ protected function formatMessage($message)
+ {
+ $user = htmlspecialchars($message->from_user);
+ $content = htmlspecialchars($message->message);
+ return "<div class=\"message\"><strong>{$user}:</strong> <span>{$content}</span></div>";
+ }
+}
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatUserManager.php b/demos/chat/protected/App_Code/ChatUserManager.php new file mode 100644 index 00000000..f8fe09cc --- /dev/null +++ b/demos/chat/protected/App_Code/ChatUserManager.php @@ -0,0 +1,63 @@ +<?php
+
+class ChatUserManager extends TModule implements IUserManager
+{
+ /**
+ * @return string name for a guest user.
+ */
+ public function getGuestName()
+ {
+ return 'Guest';
+ }
+
+ /**
+ * Returns a user instance given the user name.
+ * @param string user name, null if it is a guest.
+ * @return TUser the user instance
+ */
+ public function getUser($username=null)
+ {
+ $user=new TUser($this);
+ $user->setIsGuest(true);
+ if($username !== null)
+ {
+ $user->setIsGuest(false);
+ $user->setName($username);
+ $user->setRoles(array('normal'));
+ }
+ return $user;
+ }
+
+ /**
+ * Add a new user to the database.
+ * @param string username.
+ */
+ public function addNewUser($username)
+ {
+ $user = new ChatUserRecord();
+ $user->username = $username;
+ $user->save();
+ }
+
+ /**
+ * @return boolean true if username already exists, false otherwise.
+ */
+ public function usernameExists($username)
+ {
+ return ChatUserRecord::finder()->findByUsername($username) instanceof ChatUserRecord;
+ }
+
+ /**
+ * Validates if the username exists.
+ * @param string user name
+ * @param string password
+ * @return boolean true if validation is successful, false otherwise.
+ */
+ public function validateUser($username,$password)
+ {
+ return $this->usernameExists($username);
+ }
+}
+
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/App_Code/ChatUserRecord.php b/demos/chat/protected/App_Code/ChatUserRecord.php new file mode 100644 index 00000000..9bfb11cd --- /dev/null +++ b/demos/chat/protected/App_Code/ChatUserRecord.php @@ -0,0 +1,41 @@ +<?php
+
+class ChatUserRecord extends TActiveRecord
+{
+ public $username;
+ private $_last_activity;
+
+ public static $_tablename='chat_users';
+
+ public function getLast_Activity()
+ {
+ if($this->_last_activity === null)
+ $this->_last_activity = time();
+ return $this->_last_activity;
+ }
+
+ public function setLast_Activity($value)
+ {
+ $this->_last_activity = $value;
+ }
+
+ public static function finder()
+ {
+ return parent::getRecordFinder('ChatUserRecord');
+ }
+
+ public function getUserList()
+ {
+ $this->deleteAll('last_activity < ?', time()-300); //5 min inactivity
+ $content = '<ul>';
+ foreach($this->findAll() as $user)
+ {
+ $content .= '<li>'.htmlspecialchars($user->username).'</li>';
+ }
+ $content .= '</ul>';
+
+ return $content;
+ }
+}
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/App_Code/chat.db b/demos/chat/protected/App_Code/chat.db Binary files differnew file mode 100644 index 00000000..f8e3660f --- /dev/null +++ b/demos/chat/protected/App_Code/chat.db diff --git a/demos/chat/protected/application.xml b/demos/chat/protected/application.xml new file mode 100644 index 00000000..7153d5af --- /dev/null +++ b/demos/chat/protected/application.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?>
+<application id="Chat" Mode="Debug">
+
+ <paths>
+ <using namespace="Application.App_Code.*" />
+ <using namespace="System.Data.*" />
+ <using namespace="System.Data.ActiveRecord.*" />
+ <using namespace="System.Security.*" />
+ <using namespace="System.Web.UI.ActiveControls.*" />
+ </paths>
+
+ <modules>
+ <!-- active record database connection configuration -->
+<!-- <module class="TActiveRecordConfig" EnableCache="true"
+ Database.ConnectionString="mysql:host=localhost;dbname=prado_chat"
+ Database.Username="prado"
+ Database.Password="prado" />
+-->
+ <module class="TActiveRecordConfig" EnableCache="true"
+ Database.ConnectionString="sqlite:protected/App_Code/chat.db" />
+
+ </modules>
+
+</application>
\ No newline at end of file diff --git a/demos/chat/protected/pages/Home.page b/demos/chat/protected/pages/Home.page new file mode 100644 index 00000000..b896cd28 --- /dev/null +++ b/demos/chat/protected/pages/Home.page @@ -0,0 +1,51 @@ +<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title>Prado Chat Demo</title>
+ <link rel="stylesheet" type="text/css" href="<%~ style.css %>" />
+</head>
+
+<body>
+<com:TForm>
+<h1>Prado Chat Demo</h1>
+<noscript>Javascript is required for the chat demo to function.</noscript>
+<div id="messages" class="messages">
+ <com:TPlaceHolder ID="messageList" />
+</div>
+<div id="users" class="user-list">
+ <com:TPlaceHolder ID="userList" />
+</div>
+<div class="message-input">
+ <com:TActiveTextBox ID="userinput"
+ Columns="40" Rows="2" TextMode="MultiLine" />
+
+ <com:TActiveImageButton ID="sendButton" CssClass="send-button"
+ ImageUrl=<%~ send.gif %> OnClick="processMessage"/>
+</div>
+
+<com:TTimeTriggeredCallback OnCallback="refresh"
+ Interval="2" StartTimerOnLoad="true" />
+
+<com:TClientScript>
+ Event.observe($("<%= $this->userinput->ClientID %>"), "keypress", function(ev)
+ {
+ if(Event.keyCode(ev) == Event.KEY_RETURN)
+ {
+ if(Event.element(ev).value.length > 0)
+ new Prado.Callback("<%= $this->sendButton->UniqueID %>");
+ Event.stop(ev);
+ }
+ });
+</com:TClientScript>
+</com:TForm>
+ <com:TJavascriptLogger Visible="false"/>
+
+<div id="footer">
+Copyright © 2007 <a href="http://www.pradosoft.com">PradoSoft</a>.
+<br/><br/>
+<%= Prado::poweredByPrado() %>
+</div>
+
+</body>
+</html>
\ No newline at end of file diff --git a/demos/chat/protected/pages/Home.php b/demos/chat/protected/pages/Home.php new file mode 100644 index 00000000..65cb22dc --- /dev/null +++ b/demos/chat/protected/pages/Home.php @@ -0,0 +1,56 @@ +<?php
+
+class Home extends TPage
+{
+ public function onLoad($param)
+ {
+ $username = $this->Application->User->Name;
+ if(!$this->Application->Modules['users']->usernameExists($username))
+ {
+ $auth = $this->Application->Modules['auth'];
+ $auth->logout();
+
+ //redirect to login page.
+ $this->Response->Redirect($this->Service->ConstructUrl($auth->LoginPage));
+ }
+ }
+
+ function processMessage($sender, $param)
+ {
+ if(strlen($this->userinput->Text) > 0)
+ {
+ $record = new ChatBufferRecord();
+ $record->message = $this->userinput->Text;
+ $record->from_user = $this->Application->User->Name;
+ $record->saveMessage();
+ $this->userinput->Text = '';
+ $this->refresh($sender, $param);
+ $this->CallbackClient->focus($this->userinput);
+ }
+ }
+
+ function refresh($sender, $param)
+ {
+ //refresh the message list
+ $content = ChatBufferRecord::finder()->getUserMessages($this->Application->User->Name);
+ if(strlen($content) > 0)
+ {
+ $client = $this->Page->CallbackClient;
+ $anchor = (string)time();
+ $content .= "<a href=\"#\" id=\"{$anchor}\"> </a>";
+ $client->appendContent("messages", $content);
+ $client->focus($anchor);
+ }
+
+ //refresh the user list
+ $lastUpdate = $this->getViewState('userList','');
+ $users = ChatUserRecord::finder()->getUserList();
+ if($lastUpdate != $users)
+ {
+ $this->Page->CallbackClient->update('users', $users);
+ $this->setViewstate('userList', $users);
+ }
+ }
+}
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/pages/Login.page b/demos/chat/protected/pages/Login.page new file mode 100644 index 00000000..0d0a3fa8 --- /dev/null +++ b/demos/chat/protected/pages/Login.page @@ -0,0 +1,35 @@ +<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title>Prado Chat Demo Login</title>
+ <link rel="stylesheet" type="text/css" href="<%~ style.css %>" />
+</head>
+
+<body>
+
+<com:TForm>
+ <h1 class="login">Prado Chat Demo Login</h1>
+ <fieldset class="login">
+ <legend>Please enter your name:</legend>
+ <div class="username">
+ <com:TLabel ForControl="username" Text="Username:" />
+ <com:TTextBox ID="username" MaxLength="20" />
+ <com:TRequiredFieldValidator
+ ControlToValidate="username"
+ Display="Dynamic"
+ ErrorMessage="Please provide a username." />
+ <com:TCustomValidator
+ ControlToValidate="username"
+ Display="Dynamic"
+ OnServerValidate="checkUsername"
+ ErrorMessage="The username is already taken." />
+ </div>
+
+ <div class="login-button">
+ <com:TButton Text="Login" OnClick="createNewUser" />
+ </div>
+</com:TForm>
+
+</body>
+</html>
\ No newline at end of file diff --git a/demos/chat/protected/pages/Login.php b/demos/chat/protected/pages/Login.php new file mode 100644 index 00000000..831ab925 --- /dev/null +++ b/demos/chat/protected/pages/Login.php @@ -0,0 +1,40 @@ +<?php
+
+class Login extends TPage
+{
+ /**
+ * Check that the username is not already taken.
+ * @param TControl custom validator that created the event.
+ * @param TServerValidateEventParameter validation parameters.
+ */
+ function checkUsername($sender, $param)
+ {
+ $manager = $this->Application->Modules['users'];
+ if($manager->usernameExists($this->username->Text))
+ $param->IsValid = false;
+ }
+
+ /**
+ * Create and login a new user, then redirect to the requested page.
+ * @param TControl button control that created the event.
+ * @param TEventParameter event parameters.
+ */
+ function createNewUser($sender, $param)
+ {
+ if($this->Page->IsValid)
+ {
+ $manager = $this->Application->Modules['users'];
+ $manager->addNewUser($this->username->Text);
+
+ //do manual login
+ $user = $manager->getUser($this->username->Text);
+ $auth = $this->Application->Modules['auth'];
+ $auth->updateSessionUser($user);
+ $this->Application->User = $user;
+
+ $this->Response->redirect($auth->ReturnUrl);
+ }
+ }
+}
+
+?>
\ No newline at end of file diff --git a/demos/chat/protected/pages/config.xml b/demos/chat/protected/pages/config.xml new file mode 100644 index 00000000..c9fbdaae --- /dev/null +++ b/demos/chat/protected/pages/config.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+
+ <modules>
+ <module id="users" class="ChatUserManager" />
+ <module id="auth" class="TAuthManager" UserManager="users" LoginPage="Login" />
+ </modules>
+
+ <authorization>
+ <allow pages="Login" users="?" />
+ <allow roles="normal" />
+ <deny users="*" />
+ </authorization>
+
+</configuration>
\ No newline at end of file diff --git a/demos/chat/protected/pages/send.gif b/demos/chat/protected/pages/send.gif Binary files differnew file mode 100644 index 00000000..b3c5219d --- /dev/null +++ b/demos/chat/protected/pages/send.gif diff --git a/demos/chat/protected/pages/send.png b/demos/chat/protected/pages/send.png Binary files differnew file mode 100644 index 00000000..3025b77d --- /dev/null +++ b/demos/chat/protected/pages/send.png diff --git a/demos/chat/protected/pages/style.css b/demos/chat/protected/pages/style.css new file mode 100644 index 00000000..884aff7c --- /dev/null +++ b/demos/chat/protected/pages/style.css @@ -0,0 +1,82 @@ +body
+{
+ width: 700px;
+ margin: 2em auto;
+ font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
+}
+
+h1
+{
+ font-size: 1.5em;
+ color: #444;
+}
+
+.login
+{
+ width: 500px;
+ margin: 1em auto;
+}
+
+.login .username
+{
+ margin: 1em;
+}
+
+.login .login-button
+{
+ padding-left: 7em;
+}
+
+.messages
+{
+ width: 500px;
+ height: 300px;
+ float: left;
+ border: 1px solid ButtonFace;
+ overflow: auto;
+}
+
+.messages .message
+{
+ margin: 0.4em 1em;
+ font-size: 0.85em;
+}
+
+.user-list
+{
+ margin-left: 2px;
+ float: left;
+ width: 180px;
+ height: 300px;
+ border: 1px solid ButtonFace;
+ overflow: auto;
+ font-size: 0.85em;
+}
+
+.message-input
+{
+ float: left;
+}
+
+.message-input textarea
+{
+ margin-top: 3px;
+ padding: 0.4em 0.2em;
+ width: 493px;
+ font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
+ font-size: 0.85em;
+ height: 40px;
+}
+
+.send-button
+{
+ margin: 0.5em;
+}
+
+#footer
+{
+ clear: both;
+ font-size: 0.7em;
+ text-align: center;
+ padding-top: 50px;
+}
\ No newline at end of file diff --git a/demos/quickstart/protected/controls/TopicList.tpl b/demos/quickstart/protected/controls/TopicList.tpl index c697b0c8..40c70424 100644 --- a/demos/quickstart/protected/controls/TopicList.tpl +++ b/demos/quickstart/protected/controls/TopicList.tpl @@ -15,6 +15,7 @@ <ul>
<li><a href="?page=GettingStarted.HelloWorld">Creating First PRADO Application</a></li>
<li><a href="?page=Tutorial.CurrencyConverter">Currency Converter</a></li>
+ <li><a href="?page=Tutorial.AjaxChat">AJAX Chat Client</a></li>
<li><a href="?page=GettingStarted.CommandLine">Command Line Tool</a></li>
</ul>
</div>
diff --git a/demos/quickstart/protected/pages/Advanced/Scripts1.page b/demos/quickstart/protected/pages/Advanced/Scripts1.page index ef0b6317..7079582a 100644 --- a/demos/quickstart/protected/pages/Advanced/Scripts1.page +++ b/demos/quickstart/protected/pages/Advanced/Scripts1.page @@ -93,469 +93,4 @@ function test3() </script>
</com:TTextHighlighter>
-<h2 id="6606">Using the <tt>$A()</tt> function</h2>
-
-<p>
- The <tt>$A()</tt> function converts the single argument it receives
- into an <tt>Array</tt> object.
-</p>
-<p>
- This function, combined with the extensions for the Array class,
- makes it easier to convert or copy any enumerable list into an
- <tt>Array</tt> object. One suggested use is to convert DOM
- <tt>NodeLists</tt> into regular arrays, which can be traversed
- more efficiently. See example below.
-</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-<select id="lstEmployees" size="10" >
- <option value="5">Buchanan, Steven</option>
- <option value="8">Callahan, Laura</option>
- <option value="1">Davolio, Nancy</option>
-</select>
-
-<input type="button" value="Show the options" onclick="showOptions();" />
-
-<script type="text/javascript">
-/*<![CDATA[*/
-function showOptions()
-{
- var someNodeList = $('lstEmployees').options;
- var nodes = $A(someNodeList);
-
- nodes.each(function(node)
- {
- alert(node.nodeName + ': ' + node.innerHTML);
- });
-}
-/*]]>*/
-</script>
-</com:TTextHighlighter>
-
-<h2 id="6607">Using the <tt>$H()</tt> function</h2>
-<p>
- The <tt>$H()</tt> function converts
- objects into enumerable Hash objects that
- resemble associative arrays.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-function testHash()
-{
- //let's create the object
- var a =
- {
- first: 10,
- second: 20,
- third: 30
- };
-
- //now transform it into a hash
- var h = $H(a);
- alert(h.toQueryString());
-
- //displays: first=10&second=20&third=30
-}
-</com:TTextHighlighter>
-
-<h2 id="6608">Enumerating... Wow! Damn! Wahoo!</h2>
-<p>
- We are all familiar with for-loops. You know, create yourself an array, populate it with
- elements of the same kind, create a loop control structure (for, foreach, while, repeat, etc,)
- access each element sequentially, by its numeric index, and do something with the element.
-</p>
-<p>
- When you come to think about it, almost every time you have an array in your code it
- means that you'll be using that array in a loop sooner or later. Wouldn't it be nice
- if the array objects had more functionality to deal with these iterations? Yes, it would,
- and many programming languages provide such functionality in their arrays or equivalent
- structures (like collections and lists.)
-</p>
-
-<p>
- Well, it turns out that prototype.js gives us the Enumerable
- object, which implements a plethora of tricks for us to use when dealing with iterable data.
- The prototype.js library goes one step further and extends the
- <tt>Array</tt> class with all the methods of <tt>Enumerable</tt>.
-</p>
-
-<a name="Loops"></a>
-<h3 id="6617">Loops and iterator</h3>
-<p>
- In standard javascript, if you wanted to sequentially display the elements of an array,
- you could very well write something like this.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-<script type="text/javascript">
-/*<![CDATA[*/
-function showList()
-{
- var simpsons = ['Homer', 'Marge', 'Lisa', 'Bart', 'Meg'];
- for(i=0; i < simpsons.length; i++)
- {
- alert(simpsons[i]);
- }
-}
-/*]]>*/
-</script>
-
-<input type="button" value="Show List" onclick="showList();" />
-</com:TTextHighlighter>
-<p>
- With our new best friend, prototype.js, we can rewrite this loop like this.
-</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-function showList()
-{
- var simpsons = ['Homer', 'Marge', 'Lisa', 'Bart', 'Meg'];
- simpsons.each( function(familyMember)
- {
- alert(familyMember);
- });
-}
-</com:TTextHighlighter>
-<p>
- You are probably thinking "big freaking deal...just a weird syntax for the same old thing."
- Well, in the above example, yes, there's nothing too earth shattering going on. After all,
- there's not much to be changed in such a drop-dead-simple example. But
- keep reading, nonetheless.
-</p>
-<p>
- Before we move on. Do you see this function that is being passed as an argument
- to the <tt>each</tt> method? Let's start referring to it as an
- <b>iterator</b> function.
-</p>
-
-<h3 id="6618">Your arrays on steroids</h3>
-
-<p>
- Like we mentioned above, it's very common for all the elements in your array to be of
- the same kind, with the same properties and methods. Let's see how we can take advantage
- of iterator functions with our new souped-up arrays.
-</p>
-<p>
- Finding an element according to a criteria.
-<p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-<script type="text/javascript">
-/*<![CDATA[*/
-function findEmployeeById(emp_id)
-{
- var listBox = $('lstEmployees')
- var options = $A(listBox.options);
- var opt = options.find( function(employee)
- {
- return (employee.value == emp_id);
- });
-
- alert(opt.innerHTML); //displays the employee name
-}
-/*]]>*/
-</script>
-
-<select id="lstEmployees" size="10" >
- <option value="5">Buchanan, Steven</option>
- <option value="8">Callahan, Laura</option>
- <option value="1">Davolio, Nancy</option>
-</select>
-
-<input type="button" value="Find Laura" onclick="findEmployeeById(8);" />
-</com:TTextHighlighter>
-
-<p>
- Now let's kick it up another notch. See how we can filter out
- items in arrays, then retrieve just a desired member from each
- element.
-<p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-<script type="text/javascript">
-/*<![CDATA[*/
-function showLocalLinks(paragraph)
-{
- paragraph = $(paragraph);
- var links = $A(paragraph.getElementsByTagName('a'));
-
- //find links that do not start with 'http'
- var localLinks = links.findAll( function(link)
- {
- var start = link.href.substring(0,4);
- return start !='http';
- });
-
- //now the link texts
- var texts = localLinks.pluck('innerHTML');
-
- //get them in a single string
- var result = texts.inspect();
- alert(result);
-}
-/*]]>*/
-</script>
-
-<p id="someText">
- This <a href="http://othersite.com/page.html">text</a> has
- a <a href="#localAnchor">lot</a> of
- <a href="#otherAnchor">links</a>. Some are
- <a href="http://wherever.com/page.html">external</a>
- and some are <a href="#someAnchor">local</a>
-</p>
-<input type=button value="Find Local Links"
- onclick="showLocalLinks('someText')" />
-</com:TTextHighlighter>
-<p>
- It takes just a little bit of practice to get completely addicted to this syntax.
- Next we will go through the available functions with the following example.
-</p>
-<h1 id="6602">Enumerable Functions</h1>
-The sample data for the following examples.
-<com:TTextHighlighter Language="javascript" CssClass="source">
-var Fixtures =
-{
- Products:
- [
- {name: 'Basecamp', company: '37signals', type: 'Project Management'},
- {name: 'Shopify', company: 'JadedPixel', type: 'E-Commerce'},
- {name: 'Mint', company: 'Shaun Inman',type: 'Statistics'}
- ],
-
- Artist:
- [
- 'As I Lay Dying',
- '36 Crazyfist',
- 'Shadows Fall',
- 'Trivium',
- 'In Flames'
- ],
-
- Numbers: [0, 1, 4, 5, 98, 32, 12, 9]
-};
-
-var F = Fixtures;
-</com:TTextHighlighter>
-
-<h2 id="6609"><tt>Enumerable.each</tt> function</h2>
-<p>I used to find myself writing a lot of for loops. Although,
-Prototype does not by any means eliminate the need to do for-loops,
-it does give you access to what I consider to be a cleaner, easier to read method in each.
-<com:TTextHighlighter Language="javascript" CssClass="source">
-for(var i = 0; i < F.Numbers.length; i++)
-{
- Logger.info(F.Numbers[i]);
-}
-</com:TTextHighlighter>
-<p>
-The <tt>each</tt> function allows us to iterate over these objects Ruby style.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-F.Numbers.each(function(num)
-{
- Logger.info(num);
-});
-
-//Output
-0
-1
-4
-5
-98
-32
-12
-9
-</com:TTextHighlighter>
-
-<p>The <tt>each</tt> function takes one argument, an <b>iterator</b> function.
-This iterator is invoked once for every item in the array, and that item
-along with the optional index is passed to the iterator. So if
-we also needed the index we could do something like the code below.
-</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-F.Numbers.each(function(num, index)
-{
- Logger.info(index + ": " + num);
-});
-
-//Output
-0: 0
-1: 1
-2: 4
-3: 5
-4: 98
-5: 32
-6: 12
-7: 9
-</com:TTextHighlighter>
-
-<h2 id="6610">Hash key/value pairs</h2>
-<p>Hashes can be created by wrapping an Object (associative array) in
-<tt>$H()</tt> and can have their key/value pairs exposed.</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-$H(F.Products[0]).each(function(product)
-{
- Logger.info(product.key + ": " + product.value);
-});
-
-//Outputs
-name: Basecamp
-company: 37signals
-type: Project Management
-</com:TTextHighlighter>
-<p>
-We can also directly access the keys and values of a Hash without iterating over it.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-$H(F.Products[1]).keys();
-//Outputs name,company,type
-
-$H(F.Products[1]).values();
-//Outputs Shopify,JadedPixel,E-Commerce
-</com:TTextHighlighter>
-
-<h2 id="6611"><tt>Enumerable.collect</tt> function</h2>
-
-<p>The <tt>collect</tt> function allows you to iterate over an <tt>Array</tt> and return the
-results as a new array. Each item returned as a result of the iteration will be
-pushed onto the end of the new array.</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-var companies = F.Products.collect(function(product)
-{
- return product.company;
-});
-
-Logger.info(companies.join(', '));
-
-// Outputs
-// 37signals, JadedPixel, Shaun Inman
-</com:TTextHighlighter>
-
-<p>You can even join on the end of the block.</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-return F.Products.collect(function(product)
-{
- return product.company;
-}).join(', ');
-</com:TTextHighlighter>
-
-<h2 id="6612"><tt>Enumerable.include</tt> function</h2>
-
-<p>The <tt>include</tt> function allows you to check if a value is included in an array
-and returns true or false depending on if a match was made. Assuming I put
-up a form asking the user to name some artist in my iTunes playlist,
-we could do something like the code below. Prime candidate for some conditional madness.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-return F.Artists.include('Britney Spears'); // returns false
-</com:TTextHighlighter>
-
-<h2 id="6613"><tt>Enumerable.inject</tt> function</h2>
-
-<p>The <tt>inject</tt> function is good for getting a collective sum from an array of
-values. For instance, to add up all the numbers.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-var score = F.Numbers.inject(0, function(sum, value)
-{
- return sum + value;
-});
-
-Logger.info(score);
-//Output 161
-</com:TTextHighlighter>
-
-<p>The first argument to <tt>inject</tt> is just an initial value that
-would be added to the sum, so if we added 1 instead of 0, the output would be 162.</p>
-
-<h2 id="6614"><tt>Enumerable.findAll</tt> function</h2>
-<p>
-When given an Array, the <tt>findAll</tt> function will return an array of
-items for which the iterator evaluated to true. Basically, it allows you to
-build a new array of values based on some search criteria.
-If we wanted to find all products whose type was “E-Commerce”
-we could do something like the code below.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-var ecom = F.Products.findAll(function(product)
-{
- return product.type == 'E-Commerce';
-});
-
-Logger.info(ecom[0].company + " produces " + ecom[0].name);
-
-//Outputs
-JadedPixel produces Shopify
-</com:TTextHighlighter>
-
-<p>Note that even if only one match is made, just as in this case,
-the result is still returned as an array. In that case,
-<tt>ecom.company</tt> would return <tt>undefined</tt>.</p>
-
-<h2 id="6615"><tt>Enumerable.detect</tt> function</h2>
-<p>Unlike the <tt>findAll</tt> function, the <tt>detect</tt> function will only
-return the first item for which the expression inside
-the iterator is true. So, if we wanted to find the first number that
-was greater than 5 we’d do something like the code below.
-</p>
-<com:TTextHighlighter Language="javascript" CssClass="source">
-var low = F.Numbers.detect(function(num)
-{
- return num > 5
-});
-
-Logger.info(low);
-//Outputs 98
-</com:TTextHighlighter>
-
-<p>Even though, there are other numbers above 5 in our array, detect
-only gives us the first match back.</p>
-
-<h2 id="6616"><tt>Enumerable.invoke</tt> function</h2>
-
-<p>The <tt>invoke</tt> function allows us to pass a method as a string and
-have that method invoked. For instance, if we wanted to sort
-our array of artists we’d do something like this:</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-[F.Artists].invoke('sort')
-//Outputs 36 Crazyfist,As I Lay Dying,In Flames,Shadows Fall,Trivium
-</com:TTextHighlighter>
-
-<p>Why not just use <tt>F.Artists.sort</tt>? Well, for the example above
-we could do just that, but here is where <tt>invoke</tt> shines.</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-[F.Artists, F.Letters].invoke('sort');
-//Outputs 36 Crazyfist,As I Lay Dying,In Flames,...
-</com:TTextHighlighter>
-<p>So we invoked sort for each sub-array. Note that the code below will not work.</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-F.Artists.invoke('sort');
-</com:TTextHighlighter>
-
-<p>The reason this will not work is because it is taking each item
-in that array and trying to apply sort to it, thus if we wrote it outright,
-it would look something like this:</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-"36 Crazy Fists".sort();
-</com:TTextHighlighter>
-<p>We could however do something like this:</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-F.Artists.invoke('toLowerCase');
-//Outputs 36 crazyfist,as i lay dying,in flames,shadows ...
-</com:TTextHighlighter>
-
-<p>
-Now, what about passing arguments to the <tt>invoke</tt> function?
-The first argument passed to <tt>invoke</tt> is the method to be invoked,
-and any other arguments beyond that will be passed as arguments to the invoked method.</p>
-
-<com:TTextHighlighter Language="javascript" CssClass="source">
-F.Artists.invoke('concat', " is awesome ")
-//Outputs
-36 Crazyfist is awesome ,As I Lay Dying is awesome ,...
-</com:TTextHighlighter>
-
</com:TContent>
\ No newline at end of file diff --git a/demos/quickstart/protected/pages/Tutorial/AjaxChat.page b/demos/quickstart/protected/pages/Tutorial/AjaxChat.page new file mode 100644 index 00000000..3a17b5d4 --- /dev/null +++ b/demos/quickstart/protected/pages/Tutorial/AjaxChat.page @@ -0,0 +1,753 @@ +<com:TContent ID="body"> + <h1>Building an AJAX Chat Application</h1> + <p>This tutorial introduces the Prado web application framework's + <a href="?page=Database.ActiveRecord">ActiveRecord</a> + and <a href="?page=ActiveControls.Home">Active Controls</a> to build a Chat + web application . It is assumed that you + are familiar with PHP and you have access to a web server that is able to serve PHP5 scripts. + This basic chat application will utilizing the following ideas/components in Prado. + <ul> + <li>Building a custom User Manager class.</li> + <li>Authenticating and adding a new user to the database.</li> + <li>Using ActiveRecord to interact with the database.</li> + <li>Using Active Controls and callbacks to implement the user interface.</li> + <li>Separating application logic and application flow.</li> + </ul> + </p> + + <p>In this tutorial you will build an AJAX Chat web application that allows + multiple users to communicate through their web browser. + The application consists of two pages: a login page + that asks the user to enter their nickname and the main application chat + page. + You can try the application <a href="../chat/index.php">locally</a> or at + <a href="http://www.pradosoft.com/demos/chat/">Pradosoft.com</a>. + The main application chat page is shown bellow. + <img src=<%~ chat1.png %> class="figure" /> + </p> + + <h1>Download, Install and Create a New Application</h1> + <p>The download and installation steps are similar to those in + the <a href="?page=Tutorial.CurrencyConverter#download">Currency converter tutorial</a>. + To create the application, we run from the command line the following. + See the <a href="?page=GettingStarted.CommandLine">Command Line Tool</a> + for more details. +<com:TTextHighlighter Language="text" CssClass="source"> +php prado/framework/prado-cli.php -c chat +</com:TTextHighlighter> + </p> + + <p>The above command creates the necessary directory structure and minimal + files (including "index.php" and "Home.page") to run a Prado web application. + Now you can point your browser's url to the web server to serve up + the <tt>index.php</tt> script in the <tt>chat</tt> directory. + You should see the message "Welcome to Prado!" + </p> + + <h1>Authentication and Authorization</h1> + <p>The first task for this application is to ensure that each user + of the chat application is assigned with a unique (choosen by the user) + username. To achieve this, we can secure the main chat application + page to deny access to anonymouse users. First, let us create the <tt>Login</tt> + page with the following code. We save the <tt>Login.php</tt> and <tt>Login.page</tt> + in the <tt>chat/protected/pages/</tt> directory (there should be a <tt>Home.page</tt> + file there create by the command line tool). + </p> +<com:TTextHighlighter Language="php" CssClass="source"> +<?php +class Login extends TPage +{ +} +?> +</com:TTextHighlighter> +<com:TTextHighlighter Language="prado" CssClass="source"> +<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>Prado Chat Demo Login</title> +</head> +<body> +<com:TForm> + <h1 class="login">Prado Chat Demo Login</h1> + <fieldset class="login"> + <legend>Please enter your name:</legend> + <div class="username"> + <com:TLabel ForControl="username" Text="Username:" /> + <com:TTextBox ID="username" MaxLength="20" /> + <com:TRequiredFieldValidator + ControlToValidate="username" + Display="Dynamic" + ErrorMessage="Please provide a username." /> + </div> + <div class="login-button"> + <com:TButton Text="Login" /> + </div> +</com:TForm> +</body> +</html> +</com:TTextHighlighter> + <p>The login page contains + a <com:DocLink ClassPath="System.Web.UI.WebControls.TForm" Text="TForm" />, + a <com:DocLink ClassPath="System.Web.UI.WebControls.TTextBox" Text="TTextBox" />, + a <com:DocLink ClassPath="System.Web.UI.WebControls.TRequiredFieldValidator" Text="TRequiredFieldValidator" /> + and a <com:DocLink ClassPath="System.Web.UI.WebControls.TButton" Text="TButton" />. The resulting + page looks like the following (after applying some a style sheet). + <img src=<%~ chat2.png %> class="figure" /> + If you click on the <tt>Login</tt> button without entering any + text in the username textbox, an error message is displayed. This is + due to the <com:DocLink ClassPath="System.Web.UI.WebControls.TRequiredFieldValidator" Text="TRequiredFieldValidator" /> + requiring the user to enter some text in the textbox before proceeding. + </p> +<h2>Securing the <tt>Home</tt> page</h2> +<p>Now we wish that if the user is trying to access the main application +page, <tt>Home.page</tt>, before they have logged in, the user is presented with +the <tt>Login.page</tt> first. We add a <tt>chat/protected/application.xml</tt> configuration +file to import some classes that we shall use later. +<com:TTextHighlighter Language="xml" CssClass="source"> +<?xml version="1.0" encoding="utf-8"?> +<application id="Chat" Mode="Debug"> + <paths> + <using namespace="System.Data.*" /> + <using namespace="System.Data.ActiveRecord.*" /> + <using namespace="System.Security.*" /> + <using namespace="System.Web.UI.ActiveControls.*" /> + </paths> +</application> +</com:TTextHighlighter> +Next, we add a <tt>chat/protected/pages/config.xml</tt> configuration file to +secure the <tt>pages</tt> directory. +<com:TTextHighlighter Language="xml" CssClass="source"> +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="users" class="TUserManager" /> + <module id="auth" class="TAuthManager" UserManager="users" LoginPage="Login" /> + </modules> + <authorization> + <allow pages="Login" users="?" /> + <allow roles="normal" /> + <deny users="*" /> + </authorization> +</configuration> +</com:TTextHighlighter> +We setup the authentication using the default classes as explained in the +<a href="?page=Advanced.Auth">authentication/authorization quickstart</a>. +In the authorization definition, we allow anonymouse users to access the +<tt>Login</tt> page (anonymouse users is specified by the <tt>?</tt> question mark). +We allow any users with role equal to "normal" (to be defined later) +to access all the pages, that is, the <tt>Login</tt> and <tt>Home</tt> pages. +Lastly, we deny all users without any roles to access any page. The authorization +rules are executed on first match basis. +</p> + +<p>If you now try to access the <tt>Home</tt> page by pointing your browser +to the <tt>index.php</tt> you will be redirected to the <tt>Login</tt> page. +</p> + +<h1>Active Record for <tt>chat_users</tt> table</h1> +<p>The <com:DocLink ClassPath="System.Secutity.TUserManager" Text="TUserManager" /> +class only provides a read-only list of users. We need to be able to add or +login new users dynamically. So we need to create our own user manager class. +First, we shall setup a database with a <tt>chat_users</tt> table and create an ActiveRecord +that can work with the <tt>chat_users</tt> table with ease. For the demo, we +use <tt>sqlite</tt> as our database for ease of distributing the demo. The demo +can be extended to use other databases such as MySQL or Postgres SQL easily. +We define the <tt>chat_users</tt> table as follows. +<com:TTextHighlighter Language="text" CssClass="source"> +CREATE TABLE chat_users +( + username VARCHAR(20) NOT NULL PRIMARY KEY, + last_activity INTEGER NOT NULL DEFAULT "0" +); +</com:TTextHighlighter> +Next we define the corresponding <tt>ChatUserRecord</tt> class and save it as +<tt>chat/protected/App_Code/ChatUserRecord.php</tt> (you need to create the +<tt>App_Code</tt> directory as well). We also save the sqlite database file +as <tt>App_Code/chat.db</tt>. +<com:TTextHighlighter Language="php" CssClass="source"> +class ChatUserRecord extends TActiveRecord +{ + public $username; + public $last_activity; + + public static $_tablename='chat_users'; + + public static function finder() + { + return parent::getRecordFinder('ChatUserRecord'); + } +} +</com:TTextHighlighter> +Before using the <tt>ChatUserRecord</tt> class we to configure a default +database connection for ActiveRecord to function. In the <tt>chat/protected/application.xml</tt> +we import classes from the <tt>App_Code</tt> directory and add an +<a href="?page=Database.ActiveRecord">ActiveRecord configuration module</a>. +<com:TTextHighlighter Language="xml" CssClass="source"> +<?xml version="1.0" encoding="utf-8"?> +<application id="Chat" Mode="Debug"> + <paths> + <using namespace="Application.App_Code.*" /> + <using namespace="System.Data.*" /> + <using namespace="System.Data.ActiveRecord.*" /> + <using namespace="System.Security.*" /> + <using namespace="System.Web.UI.ActiveControls.*" /> + </paths> + <modules> + <module class="TActiveRecordConfig" EnableCache="true" + Database.ConnectionString="sqlite:protected/App_Code/chat.db" /> + </modules> +</application> +</com:TTextHighlighter> +</p> + +<h2>Custom User Manager class</h2> +<p>To implement a custom user manager module class we just need +to extends the <tt>TModule</tt> class and implement the <tt>IUserManager</tt> +interface. The <tt>getGuestName()</tt>, <tt>getUser()</tt> and <tt>validateUser</tt> +methods are required by the <tt>IUserManager</tt> interface. +We save the custom user manager class as <tt>App_Code/ChatUserManager.php</tt>. +</p> +<com:TTextHighlighter Language="php" CssClass="source"> +class ChatUserManager extends TModule implements IUserManager +{ + public function getGuestName() + { + return 'Guest'; + } + + public function getUser($username=null) + { + $user=new TUser($this); + $user->setIsGuest(true); + if($username !== null && $this->usernameExists($username)) + { + $user->setIsGuest(false); + $user->setName($username); + $user->setRoles(array('normal')); + } + return $user; + } + + public function addNewUser($username) + { + $user = new ChatUserRecord(); + $user->username = $username; + $user->save(); + } + + public function usernameExists($username) + { + $finder = ChatUserRecord::finder(); + $record = $finder->findByUsername($username); + return $record instanceof ChatUserRecord; + } + + public function validateUser($username,$password) + { + return $this->usernameExists($username); + } +} +</com:TTextHighlighter> +<p> +The <tt>getGuestName()</tt> +method simply returns the name for a guest user and is not used in our application. +The <tt>getUser()</tt> method returns a <tt>TUser</tt> object if the username +exists in the database, the <tt>TUser</tt> object is set with role of "normal" +that corresponds to the <tt><authorization></tt> rules defined in our +<tt>config.xml</tt> file. </p> + +<p>The <tt>addNewUser()</tt> and <tt>usernameExists()</tt> +method uses the ActiveRecord corresponding to the <tt>chat_users</tt> table to +add a new user and to check if a username already exists, respectively. +</p> + +<p>The next thing to do is change the <tt>config.xml</tt> configuration to use +our new custom user manager class. We simply change the <tt><module></tt> +configuration with <tt>id="users"</tt>.</p> +<com:TTextHighlighter Language="xml" CssClass="source"> +<module id="users" class="ChatUserManager" /> +</com:TTextHighlighter> + +<h1>Authentication</h1> +<p>To perform authentication, we just want the user to enter a unique +username. We add a +<com:DocLink ClassPath="System.Web.UI.WebControls.TCustomValidator" Text="TCustomValidator" /> +for validate the uniqueness of the username and add a <tt>OnClick</tt> event handler +for the login button.</p> +<com:TTextHighlighter Language="prado" CssClass="source"> +<com:TCustomValidator + ControlToValidate="username" + Display="Dynamic" + OnServerValidate="checkUsername" + ErrorMessage="The username is already taken." /> + +... + +<com:TButton Text="Login" OnClick="createNewUser" /> +</com:TTextHighlighter> +In the <tt>Login.php</tt> file, we add the following 2 methods. +<com:TTextHighlighter Language="php" CssClass="source"> +function checkUsername($sender, $param) +{ + $manager = $this->Application->Modules['users']; + if($manager->usernameExists($this->username->Text)) + $param->IsValid = false; +} + +function createNewUser($sender, $param) +{ + if($this->Page->IsValid) + { + $manager = $this->Application->Modules['users']; + $manager->addNewUser($this->username->Text); + + //do manual login + $user = $manager->getUser($this->username->Text); + $auth = $this->Application->Modules['auth']; + $auth->updateSessionUser($user); + $this->Application->User = $user; + + $url = $this->Service->constructUrl($this->Service->DefaultPage); + $this->Response->redirect($url); + } +} +</com:TTextHighlighter> +The <tt>checkUserName()</tt> method uses the <tt>ChatUserManager</tt> class +(recall that in the <tt>config.xml</tt> configuration we set the +ID of the custom user manager class as "users") to validate the username +is not taken. +</p> +<p> +In the <tt>createNewUser</tt> method, when the validation passes (that is, +when the user name is not taken) we add a new user. Afterward we perform +a manual login process: +<ul> + <li>First we obtain a <tt>TUser</tt> instance from +our custom user manager class using the <tt>$manager->getUser(...)</tt> method.</li> + <li>Using the <tt>TAuthManager</tt> we set/update the user object in the + current session data.</li> + <li>Then we set/update the <tt>Application</tt>'s user instance with our + new user object.</li> +</ul> +Finally, we redirect the client to the default <tt>Home</tt> page. +</p> + +<h2>Default Values for ActiveRecord</h2> +<p>If you try to perform a login now, you will receive an error message like +"<i>Property '<tt>ChatUserRecord::$last_activity</tt>' must not be null as defined +by column 'last_activity' in table 'chat_users'.</i>". This means that the <tt>$last_activity</tt> +property value was null when we tried to insert a new record. We need to either +define a default value in the corresponding column in the table and allow null values or set the default +value in the <tt>ChatUserRecord</tt> class. We shall demonstrate the later by +altering the <tt>ChatUserRecord</tt> with the addition of a set getter/setter +methods for the <tt>last_activity</tt> property. + +<com:TTextHighlighter Language="php" CssClass="source"> +private $_last_activity; + +public function getLast_Activity() +{ + if($this->_last_activity === null) + $this->_last_activity = time(); + return $this->_last_activity; +} + +public function setLast_Activity($value) +{ + $this->_last_activity = $value; +} +</com:TTextHighlighter> +Notice that we renamed <tt>$last_activity</tt> to <tt>$_last_activity</tt> (note +the under score after the dollar sign). +</p> + +<h1>Main Chat Application</h1> +<p>Now we are ready to build the main chat application. We use a simple +layout that consist of one panel holding the chat messages, one panel +to hold the users list, a textare for the user to enter the text message +and a button to send the message. +<com:TTextHighlighter Language="prado" CssClass="source"> +<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>Prado Chat Demo</title> +<style> +.messages +{ + width: 500px; + height: 300px; + float: left; + border: 1px solid ButtonFace; + overflow: auto; +} +.user-list +{ + margin-left: 2px; + float: left; + width: 180px; + height: 300px; + border: 1px solid ButtonFace; + overflow: auto; + font-size: 0.85em; +} +.message-input +{ + float: left; +} + +.message-input textarea +{ + margin-top: 3px; + padding: 0.4em 0.2em; + width: 493px; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + font-size: 0.85em; + height: 40px; +} +.send-button +{ + margin: 0.5em; +} +</style> +</head> +<body> +<com:TForm> +<h1>Prado Chat Demo</h1> +<div id="messages" class="messages"> + <com:TPlaceHolder ID="messageList" /> +</div> +<div id="users" class="user-list"> + <com:TPlaceHolder ID="userList" /> +</div> +<div class="message-input"> + <com:TActiveTextBox ID="userinput" + Columns="40" Rows="2" TextMode="MultiLine" /> + <com:TActiveButton ID="sendButton" CssClass="send-button" + Text="Send" /> +</div> +</com:TForm> +<com:TJavascriptLogger /> +</body> +</html> +</com:TTextHighlighter> +We have add two Active Control components: a +<com:DocLink ClassPath="System.Web.UI.ActiveControls.TActiveTextBox" Text="TActiveTextBox" /> +and a +<com:DocLink ClassPath="System.Web.UI.ActiveControls.TActiveButton" Text="TActiveButton" />. +We also added a +<com:DocLink ClassPath="System.Web.UI.WebControls.TJavascriptLogger" Text="TJavascriptLogger" /> +that will be very useful for understanding how the Active Controls work. +</p> + +<h2>Exploring the Active Controls</h2> +<p>Lets have some fun before we proceed with setuping the chat buffering. We want +to see how we can update the current page when we receive a message. First, we add +an <tt>OnClick</tt> event handler for the <tt>Send</tt> button. + +<com:TTextHighlighter Language="prado" CssClass="source"> +<com:TActiveButton ID="sendButton" CssClass="send-button" + Text="Send" OnClick="processMessage"/> +</com:TTextHighlighter > +And the corresponding event handler method in the <tt>Home.php</tt> class (we +need to create this new file too). +<com:TTextHighlighter Language="php" CssClass="source"> +class Home extends TPage +{ + function processMessage($sender, $param) + { + echo $this->userinput->Text; + } +} +</com:TTextHighlighter> +If you now type something in the main application textbox and click the send button +you should see what ever you have typed echoed in the <tt>TJavascriptLogger</tt> console. +</p> + +<p>To append or add some content to the message list panel, we need to use +some methods in the +<com:DocLink ClassPath="System.Web.UI.ActiveControls.TCallbackClientScript" Text="TCallbackClientScript" /> +class which is available through the <tt>CallbackClient</tt> property of the +current <tt>TPage</tt> object. For example, we do can do +<com:TTextHighlighter Language="php" CssClass="source"> +function processMessage($sender, $param) +{ + $this->CallbackClient->appendContent("messages", $this->userinput->Text); +} +</com:TTextHighlighter> +This is one way to update some part of the existing page during a callback (AJAX style events) +and will be the primary way we will use to implement the chat application. +</p> + +<h1>Active Record for <tt>chat_buffer</tt> table</h1> +<p>To send a message to all the connected users we need to buffer or store +the message for each user. We can use the database to buffer the messages. The +<tt>chat_buffer</tt> table is defined as follows. +<com:TTextHighlighter Language="text" CssClass="source"> +CREATE TABLE chat_buffer +( + id INTEGER PRIMARY KEY, + for_user VARCHAR(20) NOT NULL, + from_user VARCHAR(20) NOT NULL, + message TEXT NOT NULL, + created_on INTEGER NOT NULL DEFAULT "0" +); +</com:TTextHighlighter> +The corresponding <tt>ChatBufferRecord</tt> class is saved as +<tt>App_Code/ChatBufferRecord.php</tt>. + +<com:TTextHighlighter Language="php" CssClass="source"> +class ChatBufferRecord extends TActiveRecord +{ + public $id; + public $for_user; + public $from_user; + public $message; + private $_created_on; + + public static $_tablename='chat_buffer'; + + public function getCreated_On() + { + if($this->_created_on === null) + $this->_created_on = time(); + return $this->_created_on; + } + + public function setCreated_On($value) + { + $this->_created_on = $value; + } + + public static function finder() + { + return parent::getRecordFinder('ChatBufferRecord'); + } +} +</com:TTextHighlighter> +</p> + +<h1>Chat Application Logic</h1> +<p>We finally arrive at the guts of the chat application logic. First, we +need to save a received message into the chat buffer for <b>all</b> the +current users. We add this logic in the <tt>ChatBufferRecord</tt> class. + +<com:TTextHighlighter Language="php" CssClass="source"> +public function saveMessage() +{ + foreach(ChatUserRecord::finder()->findAll() as $user) + { + $message = new self; + $message->for_user = $user->username; + $message->from_user = $this->from_user; + $message->message = $this->message; + $message->save(); + if($user->username == $this->from_user) + { + $user->last_activity = time(); //update the last activity; + $user->save(); + } + } +} +</com:TTextHighlighter> +We first find all the current users using the <tt>ChatUserRecord</tt> finder +methods. Then we duplicate the message and save it into the database. In addition, +we update the message sender's last activity timestamp. The above piece of code +demonstrates the simplicty and succintness of using ActiveRecords for simple database designs. +</p> + +<p>The next piece of the logic is to retreive the users messages from the buffer. +We simply load all the messages for a particular username and format that message +appropriately (remember to escape the output to prevent Cross-Site Scripting attacks). +After we load the messages, we delete those loaded messages and any older +messages that may have been left in the buffer. +</p> +<com:TTextHighlighter Language="php" CssClass="source"> +public function getUserMessages($user) +{ + $content = ''; + foreach($this->findAll('for_user = ?', $user) as $message) + $content .= $this->formatMessage($message); + $this->deleteAll('for_user = ? OR created_on < ?', $user, time() - 300); //5 min inactivity + return $content; +} + +protected function formatMessage($message) +{ + $user = htmlspecialchars($message->from_user); + $content = htmlspecialchars($message->message); + return "<div class=\"message\"><strong>{$user}:</strong> <span>{$content}</span></div>"; +} +</com:TTextHighlighter> + +To retrieve a list of current users (formatted), we add this logic to the +<tt>ChatUserRecord</tt> class. We delete any users that may have been inactive +for awhile. +<com:TTextHighlighter Language="php" CssClass="source"> +public function getUserList() +{ + $this->deleteAll('last_activity < ?', time()-300); //5 min inactivity + $content = '<ul>'; + foreach($this->findAll() as $user) + $content .= '<li>'.htmlspecialchars($user->username).'</li>'; + $content .= '</ul>'; + return $content; +} +</com:TTextHighlighter> + +<div class="note"><b class="tip">Note:</b> +For simplicity +we formatted the messages in these Active Record classes. For large applications, +these message formatting tasks should be done using Prado components (e.g. using +a TRepeater in the template or a custom component). +</div> +</p> + +<h1>Putting It Together</h1> +<p>Nows comes to put the application flow together. In the <tt>Home.php</tt> we update +the <tt>Send</tt> buttons <tt>OnClick</tt> event handler to use the application +logic we just implemented. +<com:TTextHighlighter Language="php" CssClass="source"> +function processMessage($sender, $param) +{ + if(strlen($this->userinput->Text) > 0) + { + $record = new ChatBufferRecord(); + $record->message = $this->userinput->Text; + $record->from_user = $this->Application->User->Name; + $record->saveMessage(); + + $this->userinput->Text = ''; + $messages = $record->getUserMessages($this->Application->User->Name); + $this->CallbackClient->appendContent("messages", $messages); + $this->CallbackClient->focus($this->userinput); + } +} +</com:TTextHighlighter> +We simply save the message to the chat buffer and then ask for all the messages +for the current user and update the client side message list using a callback +response (AJAX style). +</p> + +<p>At this point the application is actually already functional, just not very +user friendly. If you open two different browser, you should be able to communicate +between the two users when ever the <tt>Send</tt> button is clicked. +</p> + +<p>The next part is perphaps the more tricker and fiddly than the other tasks. We +need to improve the user experience. First, we want a list of current users +as well. So we add the following method to <tt>Home.php</tt>, we can call +this method when ever some callback event is raised, e.g. when the <tt>Send</tt> +button is clicked. +<com:TTextHighlighter Language="php" CssClass="source"> +protected function refreshUserList() +{ + $lastUpdate = $this->getViewState('userList',''); + $users = ChatUserRecord::finder()->getUserList(); + if($lastUpdate != $users) + { + $this->CallbackClient->update('users', $users); + $this->setViewstate('userList', $users); + } +} +</com:TTextHighlighter> +</p> + +<p>Actually, we want to periodically update the messages and user list as new +users join in and new message may arrive from other users. So we need to refresh +the message list as well.</p> +<com:TTextHighlighter Language="php" CssClass="source"> +function processMessage($sender, $param) +{ + ... + $this->refreshUserList(); + $this->refreshMessageList(); + ... +} + +protected function refreshMessageList() +{ + //refresh the message list + $finder = ChatBufferRecord::finder(); + $content = $finder->getUserMessages($this->Application->User->Name); + if(strlen($content) > 0) + { + $anchor = (string)time(); + $content .= "<a href=\"#\" id=\"{$anchor}\"> </a>"; + $this->CallbackClient->appendContent("messages", $content); + $this->CallbackClient->focus($anchor); + } +} +</com:TTextHighlighter> +The anchor using <tt>time()</tt> as ID for a focus point is so that when the +message list on the client side gets very long, the focus method will +scroll the message list to the latest message (well, it works in most browsers). +</p> + +<p>Next, we need to redirect the user back to the login page if the user has +been inactive for some time, say about 5 mins, we can add this check to any stage +of the page life-cycle. Lets add it to the <tt>onLoad()</tt> stage. +<com:TTextHighlighter Language="php" CssClass="source"> +public function onLoad($param) +{ + $username = $this->Application->User->Name; + if(!$this->Application->Modules['users']->usernameExists($username)) + { + $auth = $this->Application->Modules['auth']; + $auth->logout(); + + //redirect to login page. + $this->Response->Redirect($this->Service->ConstructUrl($auth->LoginPage)); + } +} +</com:TTextHighlighter> +</p> + +<h1>Improving User Experience</h1> +<p>The last few details are to periodically check for new messages and +refresh the user list. We can accomplish this by polling the server using a +<com:DocLink ClassPath="System.Web.UI.ActiveControls.TTimeTriggeredCallback" Text="TTimeTriggeredCallback" /> +control. We add a <tt>TTimeTriggeredCallback</tt> to the <tt>Home.page</tt> +and call the <tt>refresh</tt> handler method to defined in <tt>Home.php</tt>. +We set the polling interval to 2 seconds. +<com:TTextHighlighter Language="prado" CssClass="source"> +<com:TTimeTriggeredCallback OnCallback="refresh" + Interval="2" StartTimerOnLoad="true" /> +</com:TTextHighlighter> +<com:TTextHighlighter Language="php" CssClass="source"> +function refresh($sender, $param) +{ + $this->refreshUserList(); + $this->refreshMessageList(); +} +</com:TTextHighlighter> +</p> + +<p>The final piece requires us to use some javascript. We want that when the +user type some text in the textarea and press the <tt>Enter</tt> key, we want it +to send the message without clicking on the <tt>Send</tt> button. We add to the +<tt>Home.page</tt> some javascript. + +<com:TTextHighlighter Language="javascript" CssClass="source"> +<com:TClientScript> +Event.observe($("<%= $this->userinput->ClientID %>"), "keypress", function(ev) +{ + if(Event.keyCode(ev) == Event.KEY_RETURN) + { + if(Event.element(ev).value.length > 0) + new Prado.Callback("<%= $this->sendButton->UniqueID %>"); + Event.stop(ev); + } +}); +</com:TClientScript> +</com:TTextHighlighter> +Details regarding the javascript can be explored in the +<a href="?page=Advanced.Scripts">Introduction to Javascript</a> section of the quickstart. +</p> + +<p>This completes the tutorial on making a basic chat web application using +the Prado framework. Hope you have enjoyed it. +</p> + +</com:TContent>
\ No newline at end of file diff --git a/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page b/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page index 0e54fbc2..fdce0b47 100644 --- a/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page +++ b/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page @@ -11,12 +11,12 @@ relative to the dollar. The completed application is shown bellow. <img src=<%~ example2.png %> class="figure" /> You can try the application <a href="../currency-converter/index.php">locally</a> or at - <a href="http://www.pradosoft.com/demo/currency-converter/">Pradosoft.com</a>. + <a href="http://www.pradosoft.com/demos/currency-converter/">Pradosoft.com</a>. Notice that the application still functions exactly the same if javascript is not available on the user's browser. </p> - <h1>Downloading and Installing Prado</h1> + <h1 id="download">Downloading and Installing Prado</h1> <p>To install Prado, simply download the latest version of Prado from <a href="http://www.pradosoft.com/">http://www.pradosoft.com</a> and unzip the file to a directory <b>not</b> accessible by your web server diff --git a/demos/quickstart/protected/pages/Tutorial/chat1.png b/demos/quickstart/protected/pages/Tutorial/chat1.png Binary files differnew file mode 100644 index 00000000..8288b496 --- /dev/null +++ b/demos/quickstart/protected/pages/Tutorial/chat1.png diff --git a/demos/quickstart/protected/pages/Tutorial/chat2.png b/demos/quickstart/protected/pages/Tutorial/chat2.png Binary files differnew file mode 100644 index 00000000..97cbc51d --- /dev/null +++ b/demos/quickstart/protected/pages/Tutorial/chat2.png diff --git a/framework/Data/ActiveRecord/Exceptions/messages.txt b/framework/Data/ActiveRecord/Exceptions/messages.txt index f77b2275..92bdb30f 100644 --- a/framework/Data/ActiveRecord/Exceptions/messages.txt +++ b/framework/Data/ActiveRecord/Exceptions/messages.txt @@ -9,4 +9,5 @@ ar_no_primary_key_found = Table '{0}' does not contain any primary key fiel ar_primary_key_is_scalar = Primary key '{1}' in table '{0}' is NOT a composite key, invalid value '{2} used.
ar_invalid_db_connection = Missing or invalid default database connection for ActiveRecord class '{0}', default connection is set by the DbConnection property of TActiveRecordManager.
ar_mismatch_args_exception = ActiveRecord finder method '{0}' expects {1} parameters but found only {2} parameters instead.
-ar_invalid_tablename_property = ActiveRecord tablename property '{0}::${1}' must be static and not null.
\ No newline at end of file +ar_invalid_tablename_property = ActiveRecord tablename property '{0}::${1}' must be static and not null.
+ar_value_must_not_be_null = Property '{0}::${2}' must not be null as defined by column '{2}' in table '{1}'.
\ No newline at end of file diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php index 0855fabf..68d63a23 100644 --- a/framework/Data/ActiveRecord/TActiveRecord.php +++ b/framework/Data/ActiveRecord/TActiveRecord.php @@ -210,6 +210,28 @@ abstract class TActiveRecord extends TComponent return $gateway->deleteRecordsByPk($this,(array)$keys);
}
+
+ /**
+ * Delete multiple records using a criteria.
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return int number of records deleted.
+ */
+ public function deleteAll($criteria, $parameters=array())
+ {
+ if(is_string($criteria))
+ {
+ if(!is_array($parameters) && func_num_args() > 1)
+ {
+ $parameters = func_get_args();
+ array_shift($parameters);
+ }
+ $criteria=new TActiveRecordCriteria($criteria,$parameters);
+ }
+ $gateway = $this->getRecordManager()->getRecordGateway();
+ return $gateway->deleteRecordsByCriteria($this, $criteria);
+ }
+
/**
* Populate the record with data, registers the object as clean.
* @param string new record name
diff --git a/framework/Data/ActiveRecord/TActiveRecordGateway.php b/framework/Data/ActiveRecord/TActiveRecordGateway.php index e7ea5e46..1cb1c79f 100644 --- a/framework/Data/ActiveRecord/TActiveRecordGateway.php +++ b/framework/Data/ActiveRecord/TActiveRecordGateway.php @@ -247,6 +247,20 @@ class TActiveRecordGateway extends TComponent }
/**
+ * Delete multiple records by criteria.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria search criteria
+ * @return int number of records.
+ */
+ public function deleteRecordsByCriteria(TActiveRecord $record, $criteria)
+ {
+ $meta = $this->getMetaData($record);
+ $command = $meta->getDeleteByCriteriaCommand($record->getDBConnection(),$criteria);
+ $this->raiseCommandEvent(TActiveRecordStatementType::Delete,$command,$record,$criteria);
+ return $command->execute();
+ }
+
+ /**
* Raise the corresponding command event, insert, update, delete or select.
* @param string command type
* @param TDbCommand sql command to be executed.
diff --git a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php index 74c97689..fffdb6fb 100644 --- a/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php +++ b/framework/Data/ActiveRecord/Vendor/TDbMetaDataCommon.php @@ -161,9 +161,9 @@ abstract class TDbMetaDataCommon extends TDbMetaData {
$conn->setActive(true);
$numKeys = count($this->getPrimaryKeys());
- if($numKeys===0)
- throw new TActiveRecordException('ar_no_primary_key_found',$this->getTableName());
$table = $this->getTableName();
+ if($numKeys===0)
+ throw new TActiveRecordException('ar_no_primary_key_found',$table);
if($numKeys===1)
$criteria = $this->getDeleteInPkCriteria($conn,$keys);
else
@@ -173,6 +173,21 @@ abstract class TDbMetaDataCommon extends TDbMetaData $command->prepare();
return $command;
}
+
+
+ /**
+ * SQL command to delete records by criteria
+ * @param TDbConnection database connection.
+ * @param TActiveRecordCriteria criteria object.
+ * @return TDbCommand delete command.
+ */
+ public function getDeleteByCriteriaCommand($conn, $criteria)
+ {
+ $conditions = $criteria!==null?$this->getSqlFromCriteria($conn,$criteria) : '';
+ $table = $this->getTableName();
+ $sql = "DELETE FROM {$table} {$conditions}";
+ return $this->createCriteriaBindedCommand($conn,$sql, $criteria);
+ }
}
?>
\ No newline at end of file diff --git a/framework/Data/TDataSourceConfig.php b/framework/Data/TDataSourceConfig.php index 2cf74106..93857b16 100644 --- a/framework/Data/TDataSourceConfig.php +++ b/framework/Data/TDataSourceConfig.php @@ -102,6 +102,15 @@ class TDataSourceConfig extends TModule }
/**
+ * Alias for getDbConnection().
+ * @return TDbConnection database connection.
+ */
+ public function getDatabase()
+ {
+ return $this->getDbConnection();
+ }
+
+ /**
* @param string Database connection class name to be created.
*/
public function getConnectionClass()
diff --git a/framework/Web/UI/ActiveControls/TCallbackClientScript.php b/framework/Web/UI/ActiveControls/TCallbackClientScript.php index 19e395df..b10552e8 100644 --- a/framework/Web/UI/ActiveControls/TCallbackClientScript.php +++ b/framework/Web/UI/ActiveControls/TCallbackClientScript.php @@ -275,6 +275,15 @@ class TCallbackClientScript extends TApplicationComponent }
/**
+ * Focus on a particular element.
+ * @param TControl control element or element id.
+ */
+ public function focus($element)
+ {
+ $this->callClientFunction('Prado.Element.focus', $element);
+ }
+
+ /**
* Sets the style of element. The style must be a key-value array where the
* key is the style property and the value is the style value.
* @param TControl control element or element id
diff --git a/framework/Web/UI/TPage.php b/framework/Web/UI/TPage.php index dc81ccbb..53c9b03f 100644 --- a/framework/Web/UI/TPage.php +++ b/framework/Web/UI/TPage.php @@ -340,7 +340,10 @@ class TPage extends TTemplateControl */
public function getCallbackClient()
{
- return $this->getAdapter()->getCallbackClientHandler();
+ if($this->getAdapter() !== null)
+ return $this->getAdapter()->getCallbackClientHandler();
+ else
+ return new TCallbackClientScript();
}
/**
diff --git a/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page new file mode 100644 index 00000000..acf56bfe --- /dev/null +++ b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.page @@ -0,0 +1,27 @@ +<com:TForm>
+
+<com:TCallback ID="callback1" OnCallback="method1" />
+
+ <com:TCallbackOptions ID="options1">
+ <prop:ClientSide.OnSuccess>
+ var link1 = Prado.CallbackRequest.getRequestById('<%= $this->link1->ClientID %>');
+ Prado.Callback('<%= $this->callback1->UniqueID %>', link1.getCallbackParameter());
+ </prop:ClientSide.OnSuccess>
+
+ </com:TCallbackOptions>
+
+
+ <com:TActiveLinkButton
+ ID="link1"
+ Text="Tab 1"
+ OnCallback="method2"
+ ActiveControl.CallbackParameter="test"
+ ActiveControl.CallbackOptions="options1"/>
+
+<com:TActiveLabel ID="status1" />
+
+<com:TActiveLabel ID="status2" />
+
+<com:TJavascriptLogger />
+
+</com:TForm>
\ No newline at end of file diff --git a/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php new file mode 100644 index 00000000..84099114 --- /dev/null +++ b/tests/FunctionalTests/active-controls/protected/pages/ClientSideDispatch.php @@ -0,0 +1,17 @@ +<?php
+
+class ClientSideDispatch extends TPage
+{
+
+ function method1($sender, $param)
+ {
+ $this->status1->Text = "Method 1 callback with parameter: {$param->CallbackParameter}";
+ }
+
+ function method2($sender, $param)
+ {
+ $this->status2->Text = "Method 2 callback";
+ }
+}
+
+?>
\ No newline at end of file diff --git a/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page b/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page new file mode 100644 index 00000000..7de9c6ab --- /dev/null +++ b/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.page @@ -0,0 +1,30 @@ +<com:TForm>
+
+ <com:TRequiredFieldValidator
+ ControlToValidate="rad_button_list"
+ Text="You must click one"
+ Display="Dynamic"
+ ValidationGroup="Group"
+ />
+ <br />
+ <com:TActiveRadioButtonList
+ ID="rad_button_list"
+ OnCallback="radChange">
+ <com:TListItem Value="yes" Text="Yes" />
+ <com:TListItem Value="no" Text="No" />
+ <com:TListItem Value="whynot" Text="Why not?" />
+ </com:TActiveRadioButtonList>
+ <br />
+ <com:TActiveLabel
+ ID="label"
+ Text="Choice : " />
+ <br /><br /><br />
+ <com:TActiveButton
+ ID="action_button"
+ Text="action"
+ OnCallback="action"
+ ValidationGroup="Group" />
+
+
+<com:TJavascriptLogger />
+</com:TForm>
\ No newline at end of file diff --git a/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php b/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php new file mode 100644 index 00000000..10c45aa3 --- /dev/null +++ b/tests/FunctionalTests/active-controls/protected/pages/RadioButtonListTest.php @@ -0,0 +1,28 @@ +<?php
+
+class RadioButtonListTest extends TPage
+{
+ public function radChange($sender,$param){
+ $choice = 'Choice : ';
+ switch($this->rad_button_list->SelectedValue){
+ case 'yes':
+ $choice.='Yes :-)';
+ break;
+ case 'no':
+ $choice.='No :-(';
+ break;
+ case 'whynot':
+ $choice.='Why not ???';
+ break;
+ }
+ $this->label->Text = $choice;
+ }
+
+ public function action($sender,$param){
+ $this->label->Text = 'Action...';
+ }
+
+}
+
+
+?>
\ No newline at end of file |