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">
<h4>You do not have any Todos yet</h4>
</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:
- In
js/main.js
update allrole
anddefaultRole
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 commandbin/stallion users
to add or edit existing admin users. - 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. - Everywhere you retrieve a user in
main.js
, replacetodos.all()
withtodos.filter('ownerId', myContext.user.id).all()
andtodos.forId()
withtodos.find({id: todoId, ownerId: myContext.user.id}).first()
- 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.