Simple Chat Application
This tutorial shows you how to build dynamic web pages with revIgniter on the basis of a simple chat application.
The chat consists of a Login page, where you enter a name,
and the Chat page with a list of messages and an input field.
In order to accomplish this we need the following files and a database table:
- a controller named "chat.lc", the file, that will be associated with the URI.
- four views, respectively page fragments, named "chatheader.lc", "chatcontent.lc", "chatfooter.lc" and "chatlist.lc". Of course two view files, one representing the Login page and one representing the Chat page, would do, but this is to demonstrate how multiple views are assembled and as a side-effect we avoid redundant html code.
- a model named "chatmodel.lc", the file, which deals with information in your database.
- a table in your database named "chat" to store the chat messages. We use MySQL here as the database platform.
If you have not read about controllers, views and models in the User Guide you should do so before continuing.
Note: Before we begin save the following stylesheet in assets/css/chat.css and build the table using the table structure below.
The stylesheet:
body {
font: 12pt "Lucida Grande", Lucida, Verdana, Geneva, sans-serif;
}
.page_margins {
margin-top: 50px;
}
#login {
margin-left: auto;
margin-right: auto;
width: 400px;
height: 70px;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#E3E3E3, endColorstr=#A1A1A1);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#E3E3E3, endColorstr=#A1A1A1)";
background: -moz-linear-gradient(bottom, #A1A1A1, #E3E3E3);
background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgb(161,161,161)),
color-stop(1, rgb(227,227,227))
);
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
color: #515151;
font-size: 83.33%;
}
#login form {
margin-top: 0px;
}
#login form, #login p {
padding-left: 78px;
}
#login p {
padding-top: 8px;
margin-bottom: 6px;
}
#login input#name {
width: 149px;
}
#chat {
width: 500px;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#E3E3E3, endColorstr=#A1A1A1);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#E3E3E3, endColorstr=#A1A1A1)";
background: -moz-linear-gradient(bottom, #A1A1A1, #E3E3E3);
background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgb(161,161,161)),
color-stop(1, rgb(227,227,227))
);
margin-left: auto;
margin-right: auto;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
color: #515151;
}
#chatheader {
background-color: transparent;
height: 40px;
}
p#username {
float: left;
margin-left: 20px;
font-size: 83.33%;
margin-top: 12px;
}
p#logout {
text-align: right;
width: 100px;
float: right;
margin-right: 20px;
font-size: 83.33%;
margin-top: 12px;
}
a {
text-decoration: none;
color: #367ec9;
}
a:hover {
text-decoration: underline;
}
#chatbody {
height: 214px;
overflow: auto;
background-color: transparent;
padding-top: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #48a2ff;
border-top: 1px solid #48a2ff;
clear: both;
}
#chatbody li {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#3B3B3B, endColorstr=#4A4A4A);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3B3B3B, endColorstr=#4A4A4A)";
background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgb(59,59,59)),
color-stop(0.1, rgb(107,107,107))
);
background: -moz-linear-gradient(
center bottom,
rgb(59,59,59) 0%,
rgb(107,107,107) 10%
);
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
color: #e0e0e0;
font-size: 75%;
height: 30px;
padding-left: 12px;
padding-top: 1px;
margin: 6px 20px 0 -20px;
list-style-type: none;
}
#chatfooter {
background-color: #919191;
margin-top: 20px;
-webkit-border-bottom-left-radius: 10px;
-webkit-border-bottom-right-radius: 10px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
padding: 20px;
height: 20px;
}
#userinput {
width: 360px;
float: left;
}
#submitbtn {
float: right;
width: 60px;
margin-left: 28px;
}
#footer {
font-size: 58.33%;
text-align: center;
margin-top: 80px;
}
The table:
CREATE TABLE `chat` (
`id` int(7) NOT NULL auto_increment,
`user` varchar(255) NOT NULL,
`msg` text NOT NULL,
`time` int(9) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Controller
The controller consists of two handlers commonly used in controllers and three additional ones. The first handler chat is named after the file itself and is called before any other handler. It loads all required libraries and helpers plus models and the database if needed. The second handler, named index, is the default handler, which is mandatory. This handler is called automatically if no other handler is specified in the URI. Further we need a handler, which adds new messages to the database, a handler, which gets all the messages and displays them, and finally a logout handler, which ends the chat, destroys the session data and reopens the login page.
We start with the basic prototype for a controller script:
<?lc
# PUT YOUR HANDLER NAMES INTO THE GLOBAL gControllerHandlers AS A COMMA SEPARATED LIST
put "index,chat" into gControllerHandlers
# THE CONTROLLER HANDLER
command chat
# LOAD REQUIRED LIBRAIES, MODELS, HELPERS
end chat
# THE DEFAULT HANDLER
command index
-- do something here
end index
--| END OF chat.lc
--| Location: ./application/controllers/chat.lc
----------------------------------------------------------------------
Save this script as "chat.lc" in application/controllers. This controller is associated with an URI like this: example.com/index.lc/chat/ or, if you use a .htaccess file with appropriate mod_rewrite rules: example.com/chat/ (see revIgniter URLs).
The controller handler chat is called first. So, this is a good place to load required libraries, helpers, models and the database.
As we don't want page redraws while sending new chat messages we use Ajax requests and therefore load the jQuery library:
put "1.7.2" into tJquerySettings["jqueryCurrentVersion"]
rigLoaderLoadLibrary "Jquery", tJquerySettings
To store the user name, so that all messages will be sent with the associated name, we will use a cookie-based session. So we need the Session library:
rigLoaderLoadLibrary "Session"
Now load the database. Note: If the function does not contain any information in the first parameter it will connect to the group specified in your database config file. For most people, this is the preferred method of use. Make sure that all these settings are correct, that gRigA["activeRecord"] is set to TRUE and that your database contains the "chat" table. Add the following line to the chat handler:
get rigLoadDatabase()
Further we will need the Asset helper to generate javascript and css location html code:
rigLoadHelper "asset"
So far we have not built the model, but we include it here anyway and write the corresponding code afterwards:
rigLoadModel "chatmodel"
Finally we save the page title in the global variable gData. If, in the view, enclosed in double square brackets, like [[gData["pageTitle"]]], the values of this array will be automatically combined with the view:
put "revIgniter Chat Application Tutorial" into gData["pageTitle"]
Your chat handler should now look like this:
command chat
put "1.7.2" into tJquerySettings["jqueryCurrentVersion"]
rigLoaderLoadLibrary "Jquery", tJquerySettings
rigLoaderLoadLibrary "Session"
get rigLoadDatabase()
rigLoadHelper "asset"
rigLoadModel "chatmodel"
put "revIgniter Chat Application Tutorial" into gData["pageTitle"]
end chat
Note: Mind the version number of jQuery. The jQuery library is not compatible with newer versions of jQuery. Actually it is tested against version 1.4.2 but this sample application works with version 1.7.2 too.
If no handler is specified in the URI the default handler index is called. This is the handler, which does all the work when the page is loaded the first time. With this handler we check if either the user sent a name via the login form or if there is an item "name" in the session data. Insert the following code into the index handler:
if rigSessUserdata("name") <> FALSE then
put rigSessUserdata("name") into gData["user"]
put FALSE into tLogin
else
put rigVarPost("name", TRUE) into tPOSTname
if tPOSTname <> FALSE then
rigSetSessUserdata "name", tPOSTname
put tPOSTname into gData["user"]
put FALSE into tLogin
else
put TRUE into tLogin
end if
end if
If there is an item "name" in the session data we save the name in the global variable gData and set a flag to skip the Login page and to load the Chat page instead. If the item "name" is missing in the session data we check if the user sent a name via the login form. This is done with the help of the rigVarPost function.
Note: revIgniter comes with a Cross Site Scripting Hack prevention filter which can either run automatically to filter all POST and COOKIE data that is encountered, or you can run it on a per item basis. In this case the filter is called by setting the second parameter of rigVarPost() to "TRUE".
If the POST data contains a value for "name" we save it in the session data as well as in the global variable gData and set a flag to load the Chat page, otherwise we set a flag to load the Login page.
As indicated above we use jQuery to do the Ajax requests. It is beyond the scope of this tutorial to explain the following statements, which build the jQuery part, in detail. So, if you would like to get more information please read about the Jquery library in the User Guide. Basically the code below implements:
- an Ajax request named "loadMessages" to load the messages list without refreshing the page, without caching as it is updated with every new message and those updates should be reflected
- an Ajax request to add a message to the database
- an auto-scroll function to automatically scroll the content down if the chat container overflows
- a function to handle logout requests
- and a function to continuously update the chat messages list by calling the "loadMessages" function mentioned above every 2.5 seconds
Now add the following code (mind line breaks) to the index handler:
if tLogin is FALSE then
# USER SENT A NAME
rigJQopen
# LOAD MESSAGES
rigJQfunctionOpen "loadMessages"
rigJQvar "previousScroll," & rigJQgetR("'#chatbody'", "prop", "'scrollHeight'")
put "chat/view" into tAjax["url"]
put "false" into tAjax["cache"]
put rigJQsetR("'#chatbody'", "html", "data") & \
rigJQvarR("currentScroll," & rigJQgetR("'#chatbody'", "prop", "'scrollHeight'")) & \
rigJQconditionR("currentScroll > previousScroll", rigJQanimateR("'#chatbody'", "scrollTop: currentScroll", "'normal'")) into tAjax["success"]
rigJQajax tAjax
rigJQcb
# ADD MESSAGE TO MESSAGES LIST
rigJQeventOpen "'#submitbtn'", "'click'", TRUE
rigJQxhRequest "post", "chat/addmsg", "userinput:" && rigJQgetR("'#userinput'", "attr", "'value'")
rigJQset "'#userinput'", "attr", "'value', ''"
rigJQeventClose
# LOGOUT
rigJQeventOpen "'#logout'", "'click'"
rigJQvar "logout,confirm('Are you sure you want to end the chat session?');"
rigJQcondition "logout==true", "window.location = 'logout';"
rigJQeventClose
rigJQaddStatement "setInterval (loadMessages, 2500);"
# THE FOLLOWING COMMAND RETURNS THE JQUERY SCRIPT
rigJQclose
put the result into gData["JQscript"]
# GET DATA FROM DATABASE
put getMsgData() into gData["msgList"]
end if
Note: If you use jQuery version < 1.6 replace the jQuery prop methods with the jQuery attr method.
The last statement gets the messages list from the database with the help of the getMsgData function. This function needs to be implemented in the model later.
We need to make sure that the page will not be cached. So, we set a server header, which the Output library will send for you when outputting the final rendered display. Add the following line to the index handler:
rigSetHeader "Cache-Control: no-cache"
All that is left to do is to load the view files, which we will build later. As mentioned earlier we split the page into view files, which represent the header, the content and the footer. Header and footer are identical as to the Login page and the Chat page. The content is different. So, if there is no user name specified, we load the login content, otherwise the chat content:
get rigLoadView("chatheader")
if tLogin is TRUE then
get rigLoadView("chatlogin")
else
get rigLoadView("chatcontent")
end if
get rigLoadView("chatfooter")
Your index handler should now look like this:
command index
if rigSessUserdata("name") <> FALSE then
put rigSessUserdata("name") into gData["user"]
put FALSE into tLogin
else
put rigVarPost("name", TRUE) into tPOSTname
if tPOSTname <> FALSE then
rigSetSessUserdata "name", tPOSTname
put tPOSTname into gData["user"]
put FALSE into tLogin
else
put TRUE into tLogin
end if
end if
if tLogin is FALSE then
rigJQopen
rigJQfunctionOpen "loadMessages"
rigJQvar "previousScroll," & rigJQgetR("'#chatbody'", "prop", "'scrollHeight'")
put "chat/view" into tAjax["url"]
put "false" into tAjax["cache"]
put rigJQsetR("'#chatbody'", "html", "data") & \
rigJQvarR("currentScroll," & rigJQgetR("'#chatbody'", "prop", "'scrollHeight'")) & \
rigJQconditionR("currentScroll > previousScroll", rigJQanimateR("'#chatbody'", "scrollTop: currentScroll", "'normal'")) into tAjax["success"]
rigJQajax tAjax
rigJQcb
rigJQeventOpen "'#submitbtn'", "'click'", TRUE
rigJQxhRequest "post", "chat/addmsg", "userinput:" && rigJQgetR("'#userinput'", "attr", "'value'")
rigJQset "'#userinput'", "attr", "'value', ''"
rigJQeventClose
rigJQeventOpen "'#logout'", "'click'"
rigJQvar "logout,confirm('Are you sure you want to end the chat session?');"
rigJQcondition "logout==true", "window.location = 'logout';"
rigJQeventClose
rigJQaddStatement "setInterval (loadMessages, 2500);"
rigJQclose
put the result into gData["JQscript"]
put getMsgData() into gData["msgList"]
end if
rigSetHeader "Cache-Control: no-cache"
get rigLoadView("chatheader")
if tLogin is TRUE then
get rigLoadView("chatlogin")
else
get rigLoadView("chatcontent")
end if
get rigLoadView("chatfooter")
end index
Note: If you use jQuery version < 1.6 replace the jQuery prop methods with the jQuery attr method.
Now we need a handler, which adds new messages to the database. It takes the POST data, checks if the message is not empty and, along with the session data, hands it over to the model, which is responsible for database related tasks. Add the following handler to the controller script:
command addmsg
put rigSessUserdata("name") into tName
put rigVarPost("userinput") into tMsg
if (tMsg <> FALSE) and (tMsg <> "") then
_addMsg tName, tMsg
end if
end addmsg
You may notice that the name of the model handler _addMsg has an underscore as the name prefix. This is to hide certain handlers from public access. These handlers will not be served via a URL request.
Note: Don't forget to add the handler name "addmsg" to the global variable gControllerHandlers at the top of the page.
The next handler to add to the controller script gets the data from the model, stores it in the global variable gData and loads the view chatlist.lc, which consists of the messages list only:
command view
put getMsgData() into gData["msgList"]
get rigLoadView("chatlist")
end view
Of course, later we need to make sure that the model contains a getMsgData function.
Note: Add the handler name "view" to the global variable gControllerHandlers at the top of the page.
The last handler is used to end the chat session. If there is a user name, it sends the user name and a "User has left ..." message to the model to add this data to the database. Then it destroys the session data as it is not needed anymore and loads the appropriate view files to display the login page again. Add the following code to the controller:
command logout
put rigSessUserdata("name") into tName
if tName <> FALSE then
put "User" && tName && "has left the chat session." into tMsg
_addMsg tName, tMsg
end if
rigSessDestroy
get rigLoadView("chatheader")
get rigLoadView("chatlogin")
get rigLoadView("chatfooter")
end logout
Note: Add the handler name "logout" to the global variable gControllerHandlers.
Model
The model is responsible for database related tasks. In this case our model serves two purposes: It gets messages from the database and it stores new messages in the database. As described above, the controller calls a model function named getMsgData and a model handler named _addMsg. We will now build a model consisting of these two handlers.
We start with the basic prototype of a model script only stack:
script "chatmodel"
global gRigA
on libraryStack
if (gRigA is not an array) and (the environment is "server") then
put "No direct script access allowed."
exit to top
end if
if the short name of the target <> the short name of me then
pass libraryStack
end if -- if the short name of the target = the short name of me
end libraryStack
--| END OF chatmodel.livecodescript
--| Location: ./application/models/chatmodel.livecodescript
----------------------------------------------------------------------
As specified in the chat handler of the controller script, name this file "chatmodel.livecodescript" and save it in application/models.
Add the following lines to build the getMsgData function, which is used to get messages from the database:
function getMsgData
rigDbOrderBy "id", "ASC"
put rigDbGet("chat") into tQueryResult
end getMsgData
First we set an ORDER BY clause, which orders the query result ascending by a column named "id". As this column is auto-incrementing this means that new messages will be located at the bottom and old messages at the top of the list. Then we retrieve the data from the database and store the result, an array, in a variable.
Next, we check if the number of rows of the table is greater than 0. If not, there are no messages and we simply return FALSE. Insert the following lines right after "tQueryResult":
if tQueryResult["numrows"] > 0 then
end if
return FALSE
As we don't want to keep outdated chat messages we limit the number of table rows stored to 20. We do this by deleting 11 messages if the number of rows is greater than 20. Note: These figures are arbitrary. Insert the following code right after the conditional statement:
if tQueryResult["numrows"] > 20 then
get rigDbQuery("DELETE LOW_PRIORITY FROM chat ORDER BY id ASC LIMIT 11")
rigDbOrderBy "id", "ASC"
put rigDbGet("chat") into tQueryResult
end if
Now we loop through the rows of the chat table and build the messages list adding all the necessary html tags. This list will be returned by the function. Complete the function by inserting the following lines right after the code above:
repeat with i = 1 to tQueryResult["numrows"]
put rigDbRow(i) into tRowData
put tRowData["time"] into tTime
convert tTime to short time
if tRowData["msg"] is "User" && tRowData["user"] && "has left the chat session." then
put "<li><i>(" & tTime & ")" && tRowData["msg"] & "</i></li>" & return after tMsgData
else
put "<li>(" & tTime & ") <b>" & tRowData["user"] & "</b>: " & tRowData["msg"] & "</li>" & return after tMsgData
end if
end repeat
return tMsgData
Note: The table row data is stored in an array where the keys are the table field names.This array can be accessed with the help of the rigDbRow function.
Your getMsgData function should now look like this:
function getMsgData
rigDbOrderBy "id", "ASC"
put rigDbGet("chat") into tQueryResult
if tQueryResult["numrows"] > 0 then
if tQueryResult["numrows"] > 20 then
get rigDbQuery("DELETE LOW_PRIORITY FROM chat ORDER BY id ASC LIMIT 11")
rigDbOrderBy "id", "ASC"
put rigDbGet("chat") into tQueryResult
end if
repeat with i = 1 to tQueryResult["numrows"]
put rigDbRow(i) into tRowData
put tRowData["time"] into tTime
convert tTime to short time
if tRowData["msg"] is "User" && tRowData["user"] && "has left the chat session." then
put "<li><i>(" & tTime & ")" && tRowData["msg"] & "</i></li>" & return after tMsgData
else
put "<li>(" & tTime & ") <b>" & tRowData["user"] & "</b>: " & tRowData["msg"] & "</li>" & return after tMsgData
end if
end repeat
return tMsgData
end if
return FALSE
end getMsgData
Add the following code to complete the model script:
command _addMsg pName pMsg
put pName into tData["user"]
put pMsg into tData["msg"]
put the seconds into tData["time"]
get rigDbInsert("chat", tData)
end _addMsg
This handler is called by the controller. It puts a new message along with the user name and the current time into an array and inserts this data into the "chat" table.
Now, that's all about the model script and all that is left to do is to build the view files.
View
As mentioned above, the Login page and the Chat page use the same header and footer. The content is different. So, we split the pages into three separate view files. Start with the header view and save the following code in application/views as chatheader.lc:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
This is just the doctype declaration and a meta description. Now add the page title:
<title>[[gData["pageTitle"]]]</title>
Remember: We stored the page title earlier, when we built the controller, in the global variable gData.
Add the jQuery framework:
<? return rigJQuerySource() ?>
Here, we call a Jquery library function, which returns a html string linking to jQuery.
Add the stylesheet:
<? return rigCssAsset("chat.css") ?>
This is an asset helper function, which generates a CSS asset location html code.
Add the jQuery script, the code, which is needed to do all the XHTML requests:
[[gData["JQscript"]]]
Complete the chatheader.lc script with the following lines:
</head>
<body>
<div class="page_margins">
Your chatheader.lc script should now look like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>[[gData["pageTitle"]]]</title>
<? return rigJQuerySource() ?>
<? return rigCssAsset("chat.css") ?>
[[gData["JQscript"]]]
</head>
<body>
<div class="page_margins">
There is not much to say about the footer. Just save the following code as "chatfooter.lc" in application/views:
<div id="footer">
<hr />
<p>revIgniter Chat Application Tutorial</p>
</div>
</div>
</body>
</html>
There is nothing special with the content of the Login page either. So, save the following code in application/views as "chatlogin.lc":
<div id="login">
<p>Please enter your name.</p>
<form action="http://example.com/chat/" method="post">
<label for="name">Name:</label>
<input type="text" name="name" id="name" size="20" maxlength="12" placeholder="Name" />
<input type="submit" name="loginbtn" id="loginbtn" value="Login" />
</form>
</div>
Note: Don't forget to replace the action attribute with the proper URL.
Now save the code below as "chatcontent.lc" in application/views:
<div id="chat">
<div id="chatheader">
<p id="username">Welcome, <b>[[gData["user"]]]</b></p>
<p id="logout"><a href="#">Logout</a></p>
</div>
Here we display the name of the user, which we stored in our global variable gData. Further there is a link, which is used by the jQuery script to end the chat session.
To display the messages list add the following lines:
<div id="chatbody">
<ul>
[[gData["msgList"]]]
</ul>
</div>
This unordered list is built dynamically when the page is loaded the first time or whenever the message data is updated by the jQuery script, which loads the chatlist view to replace the data in this list with new data.
To complete the chatcontent view add the HTML form, which is used to send messages:
<div id="chatfooter">
<form action="http://example.com/chat/addMsg" method="post">
<input name="userinput" type="text" id="userinput" size="64" maxlength="100" placeholder="Message" />
<input name="submitbtn" type="submit" id="submitbtn" value="Send" />
</form>
</div>
</div>
You may replace the action attribute with the proper URL, if you like, but it is not necessary because the jQuery script prevents the default action anyway. So, this attribute could even be empty. The URI is correct though. It specifies a controller named "chat" and a handler named "addMsg". Check out the jQuery script, and you will notice, that it uses this URI to post new messages.
Your chatcontent view should now look like this:
<div id="chat">
<div id="chatheader">
<p id="username">Welcome, <b>[[gData["user"]]]</b></p>
<p id="logout"><a href="#">Logout</a></p>
</div>
<div id="chatbody">
<ul>
[[gData["msgList"]]]
</ul>
</div>
<div id="chatfooter">
<form action="http://example.com/chat/addMsg" method="post">
<input name="userinput" type="text" id="userinput" size="64" maxlength="100" placeholder="Message" />
<input name="submitbtn" type="submit" id="submitbtn" value="Send" />
</form>
</div>
</div>
As mentioned earlier, the jQuery script contains a function, which loads the messages list. This is done by sending a request to chat/view. Then the view handler puts the messages list into the gData variable and loads the chatlist view, which is used by the Ajax request to replace the HTML code inside the div with the id "chatbody".
This view consists of the following three lines of code, which you should save as "chatlist.lc" in application/views:
<ul>
[[gData["msgList"]]]
</ul>
That's it, your chat application should work as expected.
Conclusion
There is still work to be done to turn this into a full featured chat application, but nonetheless this sample illustrates:
- revIgniter's approach to separate application logic from presentation permits your web pages to contain minimal scripting since the presentation is separate from the LiveCode scripting.
- By using revIgniter you don,t have to write a whole lot of LiveCode code as you don't have to write libraries from scratch. revIgniter provides you with all the features you need (almost). So, you can get right to work and accomplish a lot in the least amount of time.