Handling the Initial Request
When the browser makes its first request for the calculator application, it passes the URL /. This request is dispatched to the index.js script, located in the script directory. This script redirects the request to the URL /calculator/show by executing the following line:
library.httpserver.sendRedirect(library.httpserver.makeURL("/calculator/show"));
The httpserver library is one of many JavaScript libraries provided by Phobos. This particular library includes JavaScript functions that perform basic HTTP tasks, such as request dispatching. In this case, sendRedirect performs the same task as the sendRedirect method of HttpServletResponse: it sends a temporary redirect response to the client using the specified redirect location URL.
Creating the Controller
When the request for the URL /calculator/show comes in, the runtime looks for a controller called calculator. It does so by executing the calculator.js script. This script defines a controller package called calculator using the library.common.define function:
library.common.define(controller, "calculator", function() { ... }
Phobos supports separate namespaces for controllers, libraries and modules. Therefore, when you define a new package you have to say where you want to define it. In this case, you want to define the controller package. As shown in the preceding code, the script passes the controller namespace, the name of the package to create (calculator), and the function that contains the definition of the calculator package.
Now that the runtime has located the controller package, the next step is to instantiate a controller object. To do so, the runtime looks for a Calculator property with a function value defined inside the package:
this.Calculator = function()
The first letter of Calculator is capitalized to indicate that the function is supposed to act as a constructor. At this point, the runtime has a controller instance, so it's time for it to figure out what method to invoke. To do so, it looks at the second element in the request URL, which is show, and it queries the controller object for a function-valued property of the same name. Although JavaScript doesn't have classes, you should think of this function as a method. The controller/calculator.js code uses the following pattern to define a method:
this.Calculator = function() { this.show = function() { ... } }
Alternatively, you can define a method as follows:
this.Calculator = function() {}; this.Calculator.prototype.show = function() { ... };
Setting the Initial Parameters and Rendering the View
Now that the runtime knows to invoke the show function, let's take a look at what the show function does. First, the function (or action method) reads the values of the first operand, the second operand, the selected operation, and the total from the HTTP session, initializing them with some defaults if they are not yet defined:
var firstOperand = invocation.session.firstOperand; if (firstOperand == undefined) { /* * The first time around there won't be a current value * for the calculator, so we set it to zero. */ firstOperand = 0; } var secondOperand = invocation.session.secondOperand; if (secondOperand == undefined) { /* * The first time around there won't be a current value * for the calculator, so we set it to zero. */ secondOperand = 0; } var op = invocation.session.selectedOp; if (op == undefined) { /* * Make "add" the default operator the first time around. */ op = "add"; } var total = invocation.session.total; if (total == undefined) { total = 0; } /* * The "model" global variable is used by convention to * communicate between controller and view. */ model = { total: String(total), selectedOp: op, firstOperand: String(firstOperand), secondOperand: String(secondOperand)};
The Phobos framework creates the invocation.session object automatically. As shown in the preceding code, the values of the variables are set to zero because this is the first time the page is being requested. The operation is set to a default of "add". Finally, a global variable, called model, is initialized with these values.
Later, you'll see how the page accesses these values and passes them back to the controller. The last thing the show function does is to display the page, not by writing HTML directly, but by rendering a separate “view”, represented by calculator.ejs. It does this by using the render function from the view library:
library.view.render("calculator.ejs");
When the preceding code is executed, the render function will try to locate the resource called calculator.ejs on the path for view resources, which includes /application/view. As a result, the render function resolves to /application/view/calculator.ejs.
The ejs extension stands for Embedded JavaScript, to signify that it is an HTML file with JavaScript statements and expressions embedded inside it. When the view is rendered, the embedded code is evaluated at the appropriate time. The next section describes how to create the calculator.ejs file.
Creating the View
You create a view using an embedded JavaScript file, which is an HTML file, an XML file or a text file with JavaScript code embedded in it. The JavaScript engine supported by Phobos allows you to embed JavaScript statements or expressions inside the file.
The following code shows part of the form used to submit the operands and the selected operation on the calculator page. The table tags have been removed for better readability.
<form action=<%= library.view.quoteUrl("/calculator/compute") %> method="post"> First Operand: <input type="text" size="20" name="firstOperand" value="<%= model.firstOperand %>"/> Second Operand: <input type="text" size="20" name="secondOperand" value="<%= model.secondOperand %>"/> Total: <%= model.total %> Operator <input id="add" type="radio" name="operator" value="add">+</input> <input id="subtract" type="radio" name="operator" value="subtract">-</input> <input id="multiply" type="radio" name="operator" value="multiply">*</input> <input id="divide" type="radio" name="operator" value="divide">/</input> <input type="submit" value="Compute"/></td></tr> </form>
As the page's markup shows, the page has two text input fields. The First Operand field accepts the first operand. The Second Operand field accepts the second operand used in the calculation. The total gets its value from the model global variable described in the preceding section by using the JavaScript expression <%=model.value%>.
Following the text field tags, the page includes a set of radio button tags, which represent the different operations the user can choose to perform. Finally, the page includes an input tag that represents a button that causes the form to submit when the button is clicked.
The action attribute on the form tag uses a JavaScript expression to specify the controller function that must be invoked when the form submits. This is the calculator/compute function. The next section describes how the compute function works.
But first, let's take a look at the embedded JavaScript in the page:
<script type="text/javascript"> window.onload = function() { var selectedOp = "<%=model.selectedOp %>"; document.getElementById(selectedOp).checked = true; } </script>
Because this script is included in the page, it will be executed on the client. The function in this script gets the value of the selectedOp variable from the model global variable on the server. The client-side script uses this value to mark the correct radio button as selected in the HTML page's DOM.
This page does not use any JavaScript statements, which are identified by the <% %> delimiters (note the absence of the = sign). The embedded JavaScript engine evaluates the statements and assigns the result to a variable without sending the result to the writer. For example, the following statement will result in variable a getting the value 4:
<% var a = 4.0 %>
Getting the Request Parameters and Calculating the Result
As the preceding section explained, the data of the form on the calculator page is submitted to the URL /calculator/compute using the HTTP POST method. The compute method in the Calculator controller performs the following tasks:
Extracts the firstOperand, secondOperand, and operator parameters from the request.
Performs the appropriate arithmetic operation based on the value of the operator variable.
Updates the values in the session.
The first thing the compute method does is to use the onMethod function to restrict itself to handle only POST requests:
library.httpserver.onMethod({ POST:function(){ ... } any: function() { library.httpserver.sendNotFound(); }
According to the preceding lines of code, if a request is not a POST request, the sendNotFound function is invoked. This function sends back a 404 error.
The next task of the onMethod function is to extract the parameters from the request if the request is a POST:
POST: function() { var firstOperand = Number(request.getParameter("firstOperand")); var secondOperand = Number(request.getParameter("secondOperand")); var operator = request.getParameter("operator"); ... }
After the function has retrieved the parameter values, it calculates the result of the operation and saves it into the total variable:
total = ({ add: function(x,y) { return x+y; }, subtract: function(x,y) { return x-y; }, multiply: function(x,y) { return x*y; }, divide: function(x,y) { return y == 0 ? 0 : x/y; }, }[operator])(value, operand);
Note that the preceding code creates an object literal with function-valued properties. The rough equivalent in code running on the Java platform would be something like the following:
switch(operator) { add: value = x + y; ... }
After updating the total variable with the result of the computation, the function needs to update the total in the session so the page can access it. The next section describes how to do this.
Redisplaying the New Values in the Page
After updating the value with the result of the operation, as shown in the preceding section, the compute function updates the variables in the session, as shown in the following code. You need to save values into the session if you want to access them after a redirect occurs.
invocation.session.total = total; invocation.session.selectedOp = operator; invocation.session.firstOperand = firstOperand; invocation.session.secondOperand = secondOperand;
The final step is to re-display the new values in the page. Recall from Creating the View that the calculator.ejs page already accesses the total and selectedOp variables using expressions, as shown here:
... <script type="text/javascript"> window.onload = function() { var selectedOp = "<%= model.selectedOp %>"; document.getElementById(selectedOp).checked = true; } </script> ... <td>Total</td> <td><%= model.total %></td> ...
So, in order to return the new values to the page, all you need to do is to re-render the page. MVC design patterns recommend against returning an HTML page as the result of a POST request. Rather, the recommendation is to send back a redirect (code 303) to a URL that will produce the desired page when invoked with the GET request. With Phobos, you can do this by using the sendFound function from the httpserver library:
library.httpserver.sendFound( library.httpserver.makeUrl("/calculator/show");
FOUND is the message associated with the HTTP 303 status code.