Step 3 - Getting Todo Data From the Web Connection Server

Everything we've done so far in this tutorial has been done on the client side - the Todo list is managed as an array in memory so every time you refresh the page the list goes back to its initial state. While that works great for the initial demo it's not very useful if the values are not saved.

In this step we'll look at hooking up a couple of very simple REST API endpoints to serve the list and add and delete Todo items to mimic the functionality we have created.

I'm going to extend the REST API of the previous CustomerDemo REST Step By Step Guide by adding a few methods to the existing REST service. If you are starting from scratch I recommend you read the first part of that tutorial to see how to set up a new REST service.

Creating the Server API Methods

So we'll need to create 3 methods on the server to match the client API we've set up so far:

  • Todos (Todos.csvc - GET)
  • Add a new Todo (Todo.csvc - PUT)
  • Delete a new Todo (Todo.csvc - DELETE)

Here's what this code looks like (added to CustomerRestService.prg):

************************************************************************
*  ToDos
****************************************
FUNCTION ToDos()

IF !FILE(".\todos.dbf")
	CREATE TABLE TODOS (title c(100), descript M, entered T,completed L)

	INSERT INTO TODOS VALUES ("Load up sailing gear","Load up the car, stock up on food.",DATETIME(),.f.)
	INSERT INTO TODOS VALUES ("Get on the road out East","Get in the car and drive until you find wind",DATETIME(),.f.)
	INSERT INTO TODOS VALUES ("Wait for wind","Arrive on the scene only to find no wind",DATETIME(),.f.)
	INSERT INTO TODOS VALUES ("Pray for wind","Still waiting!",DATETIME(),.F.)
	INSERT INTO TODOS VALUES ("Sail!","Score by hitting surprise frontal band and hit it big!",DATETIME(),.F.)
ENDIF

SELECT title, descript as Description, entered, completed FROM TODOS ;
	 ORDER BY entered ;
	 INTO CURSOR Tquery

RETURN "cursor_rawarray:TQuery"
ENDFUNC
*   ToDos

************************************************************************
*  ToDo
****************************************
FUNCTION ToDo(loToDo)

*ERROR "We're not ready to accept your ToDo's just yet..."

IF !USED("ToDos")
   USE ToDos IN 0
ENDIF
SELECT ToDos

lcVerb = Request.GetHttpverb()

IF lcVerb = "PUT" OR lcVerb = "POST"
	IF VARTYPE(loTodo) # "O"
	   ERROR "Invalid operation: No To Do Item passed."
	ENDIF


    LOCATE FOR TRIM(title) == loToDo.Title
    IF !FOUND()
		APPEND BLANK
	ENDIF
	GATHER NAME loTodo MEMO	
	
	*** Fix for differing field name
	REPLACE descript WITH loTodo.description
ENDIF

IF lcVerb = "DELETE"
   lcTitle = Request.QueryString("title")
   LOCATE FOR TRIM(title) == lcTitle
   IF !FOUND()
      ERROR "Invalid Todo - can't delete."
   ENDIF
   
   DELETE FOR TRIM(Title) == lcTitle
   RETURN null
ENDIF

RETURN loTodo
*   ToDo

The code is pretty straight forward. The Todos method simply retrieves a cursor and returns that as JSON optionally creating the table if it doesn't exist. The ToDo method is overloaded to handle adding, updating and deleting from a single URL based on the HTTP verb. While you can have separate methods for each of those, it's common practice in REST APIs to overload the nouns (Todo) with multiple actions that can be performed on them (POST,PUT,DELETE).

Let's make sure the server is running and working and then navigate to:

http://localhost/customerdemo/todos.csvc

to check for the JSON response of customers.

Likewise you can test an update request - here I use West Wind Web Surge to fire the request:

Retrieving the Todos From the Server - Take 1

We'll start with the simple way to access the todos using code in the pageController. Lets add a function called loadTodos() like this:

function loadTodos() {
    $http.get('todos.csvc')
        .success(function(todos) {
            vm.todos = todos;
        })
        .error(parseHttpError);
}

and let's make sure we call this function from the initialize() function so that the list is loaded when the page is loaded into the browser:

function initialize() {
    loadTodos();
}

Remember in Step 1 we talked about dependency injection and injecting services? Well, here I'm using the $http service in loadTodos(), which is injected to make the HTTP call to the Web Connection REST service.

$http has methods for most HTTP verbs (.get(), .post(), .put(), .delete() etc.) and the result from these requests is a Promise object, which is an object that wraps an asynchronous call to the server. Promises resolve to success or failure handlers that you implement and that fire whenever the async operation - the HTTP call in this case - completes.

JavaScript Promises

Promises are objects that represent an asynchronous operation. A promise wraps an async operation and provides methods that can attach to success and failure handlers - typically using .then() and .catch() although some objects like $http also support .success() and .error(). Multiple handlers can be attached to a single promise and because a promise is an object the promise can be passed around an application. Promises are a more sophisticated alternative to callbacks passed to methods and they provide distinct advantages because you pass the promise around an application and attach multiple handlers.

I use .success() to receive the ToDo list our server produced which is returned as an array parameter to the function. I can then simply assign this value to vm.todos which causes the data bound list to update in the UI.

We get the same results as before, but now with our server loaded list instead of the static hand coded array.

Adding a new ToDo

The process is very similar to add a new Todo to the server:

function postTodo(todo) {
    $http.post('todo.csvc', todo)
        .success(function(todo) {
            showmessage("Todo updated.");
        })
        .error(parseHttpError);

}

As before I use the $httpservice to make an HTTP call to the server - this time a POST operation and I'm sending the active ToDo object to the server as a parameter. The REST service can pick up the Todo object and add it to the database.

Ignore the showMessage() and parseHttpError() helpers for a second - I'll come back to that shortly. They are used to display status information on the form.

We still need to hook up this method so it gets called when we add a new Todo. We can do this by simply adding the code to our existing addToDo logic:

function addTodo() {
    // copy the tod
    var td2 = $.extend({}, vm.activeTodo);
   
    // *** Now also post it to the server
    postTodo(td2);

    // and add it to the model array
    vm.todos.splice(0, 0, td2);

    // clear out display todo
    vm.activeTodo.title = null;
    vm.activeTodo.description = null;
}

Try it out by running the form, adding a new Todo, and then reloading the page - the form should now load the updated list which includes the added todo.

Not quite the Promised Land - displaying Errors

There's a small issue with this code, that you might have missed. The problem is that the result of the HTTP call is asynchronous and in the addToDo() we are not waiting for the response to come back. If something doesn't work on the server, the new ToDo is added to the page, even if it fails on the server, which is not what we want. Rather we want to see an error message and no new items added to the list.

Error Display

Let's start with the error display. Let's add some HTML we can use to display messages.

<div class="alert alert-warning alert-dismissable"
     ng-show="vm.message">
    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
    {{ vm.message }}
</div>

This creates a Bootstrap alert component that lets us display error information. The box only shows if a vm.message property has a value, otherwise the value is hidden. This is done with the ng-show directive which if true shows the element, otherwise it's hidden.

Let's make sure our model has a .message property:

vm.message = null;

Initially the value is null, so the alert box doesn't show.

Let's add a function in JavaScript that lets us more easily set the value:

function showMessage(msg) {
    vm.message = msg;
    $timeout(function () {
        vm.message = null;
    }, 5000);
}

This lets me display a message with a single line of code. The message is automatically removed after 5 seconds.

Note I used $timeout service which is another injected value on the controller. $timeout is a wrapper around the JavaScript setTimeout() function which delay executes code. $timeout notifies Angular when the timeout timeout function call is complete so that Angular can check for model changes. Angular has many functions that wrap common DOM/JavaScript operations with its own versions so that it can check for model changes that otherwise it would not know about.

Ok - now let's use showMessage() in the code that

.success(function(todo) {
    showMessage("Todo Updated.");
})
.error(function() {
    showMessage("Todo not updated...");
});

Now when you update the Angular item you should see the 'Todo Updated' message.

Handling Errors

What about errors if the HTTP request fails?

Go into the server Process class and change the ToDo method with the following code at the very top:

ERROR "We're not ready to accept your ToDo's just yet..."

This causes the save request to fail.

In order to handle this error we an implement that .error handler on the $http.post() call:

.error(function() {
    showMessage("Todo not updated...");
});

which is easy enough. But, wouldn't it be nice if we could pick up the error message that the server sent?

The result request from the server looks like this (if DebugMode is off in the Web Connection Server - otherwise the code stops on the ERROR command):

HTTP/1.1 500 Server Error
Content-Type: application/json;charset=utf-8
RequestId: 72_8cc9a288
Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, *
Access-Control-Allow-Credentials: true

{"isCallbackError":true,"message":"Property TODO is not found."}

The server throws an error, but it also provides an error message which is the string we provided as part of the ERROR, so it's possible to pick this up.

You can automatically try and parse errors using a helper in the ww.angular.js script file, which includes a .parseHttpError() function. It produces a parsed error message that has a .message property that includes a message from passed error information, or the string based HTTP error message (Access Denied, Server Error etc.).

You can call ww.angular.parseHttpError() like this:

$http.post('todo.csvc', todo)
    .success(function(todo) {
        showMessage("Todo Updated.");
    })
    .error(function() {
        var error = ww.angular.parseHttpError(arguments);
        vm.message = "An error occurred: " + error.message;
    });

You can make this even easier by creating a helper method in the controller that wraps this code:

function parseHttpError() {
    var error = ww.angular.parseHttpError(arguments);
    vm.message = "An error occurred: " + error.message;            
}

And you can then replace the error function with this code anywhere you have an error handler that simply needs to display a message:

.error(parseHttpError)

Now when you run the input you'll see the error message displayed. Yay!

Async Error Handling

But if you look closely we still have another problem here - the error is now displaying but the input fields are cleared too, which is not right. What we want to see is the input fields still filled if the Todo that couldn't be added.

The problem is this code here that makes the $http call and doesn't wait for the result before clearing the fields:

function addTodo() {
    // copy the tod
    var td2 = $.extend({}, vm.activeTodo);
   
    // now also post it to the server
    postTodo(td2);

    // and add it to the model array
    vm.todos.splice(0, 0, td2);

    // clear out todo
    vm.activeTodo.title = null;
    vm.activeTodo.description = null;
}

Notice that this code goes ahead and adds the item and clears the fields, even though the update failed. Because the postTodo() makes an async Http call, the array update and title clearing occurs immediately before the Http call returns - effectively always which is not right.

To fix this we'll need to do two things. First we need to return the Promise from the postToDo() function like this so we can let the caller know.

function postTodo(todo) {
    return $http.put('todo.csvc', todo)
        .success(function(todo) {
            showMessage("Todo Updated.");
        })
        .error(function() {
            var error = ww.angular.parseHttpError(arguments);
            vm.message = "An error occurred: " + error.message;
        });
}

Notice the return in the first line, which returns the promise back to the caller.

In addToDo() you can now capture the Promise and add your own .success() and .error() handlers:

function addTodo() {
    // copy the tod
    var td2 = $.extend({}, vm.activeTodo);

    // now also post it to the server
    postTodo(td2)
        .success(function() {

            // and add it to the model array
            vm.todos.splice(0, 0, td2);

            // clear out todo
            vm.activeTodo.title = null;
            vm.activeTodo.description = null;
        });
}

Now, if the $http call fails the new Todo is not added to the list and the fields aren't cleared because the .success() handler never fires. Instead you see the error message which is set by the $http.error() handler we added in the postToDo() function. On success, both .success() handlers in the postToDo() and addToDo() are fired so the result value is assigned to the model, and the code to update the UI fires to handle the result.

This demonstrates one of the important features of Promises which is that you can attach multiple handlers to the same event ($http completion in this case).

Now when you run the code and see the error it should look like this. Really Yay!

Summary

You've now seen the basics of how to create a really simple Angular application that displays list of items, lets add and remove items, manage basic user input, as well as making $http calls to retrieve data from the server.

In this step you've learned

  • How to create a couple of simple REST service methods in a Web Connection Server
  • How to use the $http Service to interact with a server REST service
  • How to use Promises to manage asynchronous callbacks
  • How to handle errors and display them on a page effectively

© West Wind Technologies, 1996-2019 • Updated: 02/07/16
Comment or report problem with topic