In this code lab we will build a single page application with a 3rd party integration. Single Page Applications are websites which aim to offer an ‘app like' behavior. This is mainly achieved by providing experiences which do not cause page loads(reloads or redirecting), hence the name "Single Page". Instead we rely on javascript to show different parts of our application and perform AJAX to retrieve and send data to an application server.
This lab presents a client side rendered alternative implementation of the app built in lab 4 which was server side rendered. For your project you may choose either architecture.
Follow the link below to get started.
When creating large web applications it is desirable to split your javascript into files to improve the manageability of the code base. util.js
contains two functions which would be reused at different parts of the application.
util.js
contains three things:
server : a variable which stores the url of our application server.
Note: you will need to replace this with your fork of the completed lab 3 repl.
toast() : a function which makes use of materialize to flash messages to the user
sendRequest()
sendRequest() does the following:
The default behavior of an HTML5 form submission is to have the browser make an HTTP request to the url provided in the action attribute of the form. However, this would cause a redirect and thus is not allowed in a SPA.
Instead we let javascript interrupt the submission, get the data from the form and send the http request in the background.
Update signup.js
with the following.
async function signup(event){
event.preventDefault();//prevent page redirect
let form = event.target;
let fields = event.target.elements;
let data = {
username: fields['username'].value,
email: fields['email'].value,
password: fields['password'].value,
}
//reset form
form.reset();
//send data to application server
let result = await sendRequest(`${server}/signup`, 'POST', data);
if('error' in result){
toast("Login Failed: "+result['error']);//show error message
}else{
toast("Logged Successful");
window.location.href= 'index.html';//redirect the page
}
}
//attach signup to submit event of form
document.forms['signUpForm'].addEventListener('submit', signup);
signup() retrieves the data from the form and uses sendRequest() to send the data to the application server. The function also uses toast() to flash an appropriate message based on the response of the server. Then redirects the browser by changing window.location.href
The form in signup.html should now be functional and redirect to the login page when a user is successfully created.
login.js already has code similar to signup.js which submits the form and submits to the application server. You can now log in with the user credentials given in the sign up page. You open the application tab in chrome dev tools (F12) to inspect the JWT in localStorage.
Software applications are usually comprised of multiple views/screens, for many websites this is achieved by hosting multiple web pages linked to each other. Websites of this kind are sometimes referred to as multi-page applications.
However, in single page applications the markup for all of our views are either
INFO 1601 Lab 9 is an example of a SPA using approach 2 where ajax was used to navigate to different sections of the application.
In this lab we use materialize css tabs to implement the 1st approach. The markup for both of our tabs exist in index.html but materialize would only show the contents of one tab at a time.
index.html
By using tabs we can add multiple views to index.html. Note that materialize can only do this if the tab elements have been properly initialized in javascript.
app.js
Update the contents of the todo tab (div with id todo) with the following:
index.html
<section id="todo" class="col s12 container">
<div class="card" >
<div class="card-content">
<span class="card-title">Create Todo</span>
<form name="addForm">
<div class="input-field">
<input type="text" name="addText" placeholder="enter to-do" class="materialize-textarea"></textarea>
<label for="addText">Enter todo Text</label>
</div>
<input class="btn white-text" type="submit" value="SAVE" />
</form>
</div>
</div>
<ul class="collection with-header" id="result"></ul>
</section>
The markup should add a form to the page.
Applications are described as dynamic when a scripting language is used to dynamically write html to the page. Often this is done to present data which is stored in a database.
As web applications run on the browser they typically do not talk to databases directly for security reasons. Instead an application server would sit on the internet, query the database and expose the desired data via a url for our web applications.
The set of urls supported by an application server is often referred to as a web service API (Application Programming Interface) as they provide an interface for our web applications to the database.
Web applications rely on AJAX (Asynchronous Javascript & XML) which is a model whereby javascript makes an HTTP request to an application server then dynamically writes html with the data in the response.
https://lab5-completed.herokuapp.com/todos is an endpoint of the flask application from lab5 deployed on heroku. Below is the response when an http request is made to the endpoint however the data is restricted to authenticated users
Update app.js with the following functions which perform ajax to render the todo data onto the page.
apps.js
async function displayTodos(data){
let result = document.querySelector('#result');//access the DOM
result.innerHTML = '';//clear result area
let html = '';//make an empty html string
if("error" in data){//user not logged in
html+= `
<li class="card collection-item col s12 m4">
<div class="card-content">
<span class="card-title">
Error : Not Logged In
</span>
</div>
</li>
`;
}else{
for(let todo of data){
html+= `
<li class="card collection-item col s12 m4">
<div class="card-content">
<span class="card-title">${todo.text}
<label class="right">
<input type="checkbox" data-id="${todo.id}" onclick="toggleDone(event)" ${todo.done ? 'checked': ''} />
<span>Done</span>
</label>
</span>
</div>
<div class="card-action">
<a href="#" onclick="deleteTodo('${todo.id}')">DELETE</a>
</div>
</li>
`;//create html for each todo data by interpolating the values in the todo
}
}
//add the dynamic html to the DOM
result.innerHTML = html;
}
async function loadView(){
let todos = await sendRequest(`${server}/todo`, 'GET');
displayTodos(todos);
}
loadView();
We haven't created any user data so the code added wouldn't have any visual effect just yet.
Next we add the functionality to create user data.
Add the following functions to app.js.
async function createTodo(event){
event.preventDefault();//stop the form from reloading the page
let form = event.target.elements;//get the form from the event object
let data = {
text: form['addText'].value,//get data from form
done: false,// newly created todos aren't done by default
}
event.target.reset();//reset form
let result = await sendRequest(`${server}/todo`, 'POST', data);
if('error' in result){
toast('Error: Not Logged In');
}else{
toast('Todo Created!');
}
loadView();
}
//attach createTodo() to the submit event of the form
document.forms['addForm'].addEventListener('submit', createTodo);
The form should now be functional and newly created todos should render on the page with a message.
Applications can also support updating and deleting data, this is done by having javascript make the appropriate http request in the background then reloading the data from the database so the user can see the result.
Add the following to scripts.js
async function toggleDone(event){
let checkbox = event.target;
let id = checkbox.dataset['id'];//get id from data attribute
let done = checkbox.checked;//returns true if the checkbox is checked
let result = await sendRequest(`${server}/todo/${id}`, 'PUT', {done: done});
let message = done ? 'Done!' : 'Not Done!';
toast(message);
}
async function deleteTodo(id){
let result = await sendRequest(`${server}/todo/${id}`, 'DELETE');
toast('Deleted!');
loadView();
}
When previewing the app you can observe functioning updates and deletion of the todos.
Lastly we implement a simple logout function by deleting the JWT from localStorage then redirecting the page. The logout click is already set to call the login function on click.
Update app.js with the following
function logout(){
window.localStorage.removeItem('access_token');
window.location.href ="index.html";
}
Clicking the link should now take you back to the login page. If you navigate to app.html without logging in you should see the following error message.
Attempting to create a todo while not logged in should also flash an error message.
We rely on data attributes to send data to our elements. Then using the event object we can get the element in our event handlers. From there, we access the data by using the element's dataset property.
Example
Understanding this pattern can go a long way in the implementation of the view logic of our web applications.
Single Page vs Multi Page Apps
https://codeburst.io/jwt-authorization-in-flask-c63c1acf4eeb
https://pythonbasics.org/flask-cookies/
https://flask.palletsprojects.com/en/1.1.x/security/
https://realpython.com/token-based-authentication-with-flask/
https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html