Warning! This documentation is a work in progress. Expect things to be out of date and not actually work according to instructions.

Tutorial: A Todo List Web Application with Javascript and Stallion

1. Make the initial set up

Install Java 1.8u40 or greater, create a folder for your project todo, create a folder called bin inside your project folder, and download the Stallion binary into that folder. For complete instructions, see the first section of creating a simple website.

2. Generate project scaffolding.

From your project folder, run bin/stallion new and choose “Javascript Site” as the project type, and follow the wizard.

3. Run the server

Use the command bin/stallion -autoReload -devMode=true. The “-autoReload” flag will automatically restart the application context every time you save a file in the js or conf folder. This will allow you to see your changes without manually rebooting. Go to http://localhost:8090/ and you should see a “Hello World” page.

4. Adjust the “Hello, world” endpoint

Open the file js/main.js. You should see a “screens” object with child key of “home”. Replace that section with this code:

    var screens = {
        home: {
            route: '/',
            produces: 'text/html',
            role: 'anon',
            params: [stallion.queryParam("name")],
            handler: function(name) {
                var ctx = {
                    name: name || "world"
                };
                return stallion.renderTemplate("app.jinja", ctx);
            }
        }
    };

All this endpoint does is render a template “app.jinja” using the passed in context. We also passed in the query parameter “name” into the context, using world as a default if name is undefined.

Then open templates/app.jinja and replace “Hello world” with “Hello { name }}”.

Go to http://localhost:8090/?name=Mike and you should see a “Hello, Mike” instead of “Hello world.”

5. Add a data model and API for Todo items

Add a new section to js/main.js to define our Todo list items:


/** * Models */ (function() { todoApp.todos = stallion .modelRegistration() .columns({ title: new stallion.StringCol(), dueDate: new stallion.DateTimeCol(), createdAt: new stallion.DateTimeCol({nowOnCreate: true}), updatedAt: new stallion.DateTimeCol({nowOnUpdate: true}), completed: new stallion.BooleanCol(), subtasks: new stallion.ListCol(), extra: new stallion.MapCol(), ownerId: new stallion.LongCol({alternativeKey: true}), url: new stallion.FunctionCol(function(self) { return myContext.site.url + "/todo/" + self.id + "/" + stallion.GeneralUtils.slugify(self.title); }) }) .bucket('todos') .register(); })();

Then add a new section to define basic CRUD API endpoints.


/** * API endpoints */ (function() { var api = { meta: { baseRoute: '/api/v1/todos', defaultProduces: 'application/json', defaultRole: 'ANON' }, listTodos: { route: '/list', handler: function() { return todoApp.filterChain().all(); } }, getTodo: { route: '/get/:todoId', params: [stallion.pathParam('todoId')], handler: function(todoId) { return todoApp.forIdOrNotFound(todoId); } }, updateTodo: { route: '/update/:todoId', params: [stallion.pathParam('todoId'), stallion.mapParam()], handler: function(data) { var todo = todoApp.forIdOrNotFound(todoId); todo.title = data.title || todo.title; if (data.subtasks !== null && data.subtasks !== undefined) { todo.subtasks = data.subtasks || []; } todoApp.todos.save(todo); return todo; } }, newTodo: { route: '/new', params: [stallion.mapParam()], handler: function(data) { if (!data.title) { stallion.raiseClientException("Each todo must have a title"); } var todo = todoApp.todos.newModel(); todo.title = data.title || ''; todo.subtasks = data.subtasks || []; todoApp.todos.save(todo); return todo; } } }; stallion.registerEndpoints(api); }());

Test out the endpoints by adding a todo item:

curl -v -XPOST -H "Content-type: application/json" -d "{\"title\": \"Go through Stallion tutorial\"}" http://localhost:8091/api/v1/todos/new

5. Add todos to the home page

Edit templates/app.jinja and add the following to the main section:

<div class="post-description">
  {% set myTodos = todos.all() %}
  {% if not myTodos %}
  <h4>You do not have any Todos yet</h4>
  {% else %}
  <h4>My todos</h4>
  {% endif %}
  {% for todo in myTodos %}
  <div class="todo" data-todo-id="{ todo.id }}">
    <h4 class="todo-title" {% if todo.completed %}style="text-decoration: line-through"{% endif %}>
      <input type="checkbox" {% if todo.completed %}checked{% endif %}>
      { todo.title }}
    </h4>
    {% if todo.dueAt %}
    <div><em>due at</em> { dateUtils.formatLocalDate(todo.dueAt) }}</div>
    {% endif %}
  </div><!--end todo-->
  {% endfor %}
</div>

Reload the home page, and you should see the Todos you created via CURL.

6. Create a way to mark a Todo as done

Open js/main.js and add an endpoint to the api object for marking one or more todos as completed:

markComplete: {
    route: '/mark-complete-not-complete',
    method: 'POST',
    params: [stallion.bodyParam('todoIds'), stallion.bodyParam('completed')],
    handler: function(todoIds, completed) {
        if (!todoIds) {
            stallion.raiseClientException("You must pass in a list of todoIds", 400);
        }
        todoIds.forEach(function(todoId) {
            var todo = todos.forId(todoId);
            todo.completed = completed;
            todos.save(todo);
        });
        return true;
    }
}

The stallion.bodyParam method tells the endpoint to parse the POST body into a key/value map, and then get the value for the key todoIds and then completed and them to the handler function.

stallion.raiseClientException will short-circuit the request and return a 400 error with the message to the client.

Now we have to add browser javascript for marking a todo item as completed. Open assets/site.js and add the following code:

(function() {
    var todo = {};

    todo.init = function() {
        $(".todo input[type='checkbox']").click(function() {
            var shouldCheck = $(this).is(':checked');
            $todo = $(this).parents('.todo');
            var todoId = parseInt($todo.attr('data-todo-id'), 10);
            if (shouldCheck) {
                $.ajax({
                    method: 'POST',
                    url: '/api/v1/todos/mark-complete-not-complete',
                    data: JSON.stringify({todoIds: [todoId], completed: true}),
                    success: function() {
                        $todo.find('.todo-title').css('text-decoration', 'line-through');                        
                    },
                    contentType: "application/json"
                });
            } else {
                $.ajax({
                    method: 'POST',
                    url: '/api/v1/todos/mark-complete-not-complete',
                    data: JSON.stringify({todoIds: [todoId], completed: false}),
                    success: function() {
                        $todo.find('.todo-title').css('text-decoration', 'none');
                    },
                    contentType: "application/json"
                });
            }
        });
    };
    
    $(document).ready(todo.init);
})();


Now if you go to your home page, you should be able to click the check box and mark a todo as done, and you will see it strike-through. If you reload the page, it should still be struck through.

7. Add authentication

Right now, all “todos” are viewable by every random website visitor. To restrict adding todos to logged in users, and to associate all todos with an account, do the following:

  1. In js/main.js update all role and defaultRole attributes to be ‘member’ instead of ‘anon’. Go to the home page, and you should be prompted to login. You can login with the user you created in step 2. If you forgot your password, or you did not create a user, you can run the command bin/stallion users to add or edit existing admin users.
  2. Everywhere you save a user add the line: todo.ownerId = myContext.user.id. myContext is a global variable containing the current application and request context, including the actively logged in user.
  3. Everywhere you retrieve a user in main.js, replace todos.all() with todos.filter('ownerId', myContext.user.id).all() and todos.forId() with todos.find({id: todoId, ownerId: myContext.user.id}).first()
  4. Everywhere you retrieve todos in the template, use { todos.filter('ownerId', user.id).all() }}

All todos should only be seen by the current user, and new todos will be associated with that user.

8. Next steps

Those are the basics, read the rest of the documentation for details on each of the above, and for learning about more extensive features.

© 2018 Stallion Software LLC