Why Javascript
Currently, we have learned how to use Python for the back-end, using Flask microframework to get the app up and running. (We can also use Node.js for the back-end code, but that’s something we’ll learn later.)
It is useful to differentiate code that is run by the client (front-end) and the server (back-end). These are some considerations of how you might partition the code:
- Client-side processes reduce load on server, and are often faster.
- Server-side processes hide the architectural layers you do not want exposed to the client.
JavaScript is a programming language that was developed to be run inside a web-browser, and modern browsers have JS engines (Chrome’s V8 and Firefox’s SpiderMonkey) that can execute the code very quickly. There are many versions of JS, and we will be using ES6 in CS50W.
As with CSS stylesheets, JS can be saved as a separate file and referenced in the HTML page:
<script src="main.js"></script>
In the JS file we can write the functions that will be manipulating the DOM (Document Object Model) when certain events happen.
JavaScript events include:
onmouseover: triggers when an element is hovered overonclick: triggers when an element is clickedonkeydown: triggers when a key is pressedonkeyup: triggers when a key is releasedonload: triggers when a page is loadedonblur: triggers when an object loses focus (when moving away from an input box, for example)
Let’s look at a simple DOM manipulation function:
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('button').onclick = count;
});
let counter = 0;
function count() {
count++;
document.querySelector('#counter').innerHTML = counter;
if (counter % 5 === 0) {
alert(`Counter is at ${counter}!`);
}
}
- document refers to the webpage currently displayed.
- addEventListener listens to events (in the first one, ‘listening’ for webpage to load before calling the nested function
- JS uses higher order functions, so functions can be passed like values (eg: count). The function being passed is called a ‘callback function’. In the above case, the callback sets the onclick property of the button element to the count function.
- querySelector(”) is a function that searches through your DOM for a CSS selector and returns the first result.
- querySelectorAll(”) returns all results.
- innerHTML is the content contained within the element’s tags.
Variables
- const – constant variable, can’t be redefined
- let – scoped to the innermost pair of curly braces
- var – scoped to the function it is in
Arrow functions
You can use arrow/ anonymous functions in ES6:
() => { alert('Hello World')}
x => x *2;
Tasklist Demo
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('#new-task').onsubmit = () => {
// Create new item for list
const li = document.createElement('li');
li.innerHTML = document.querySelector('#task').value;
// Add new item to task list
document.querySelector('#tasks').append(li);
// Clear input field
document.querySelector('#task').value = '';
// Stop form from submitting
return false;
};
});
This creates a tasklist that adds a task on a list when submitted.
Integrating JavaScript with Python and Flask
- Ajax, is used to get more information from server without needing to reload an entirely new page. As an example, Ajax can be used with the currency conversion example from last week to display a conversion without needing to load a new page. This is not done by pre-loading all possible exchange rates, but by making an Ajax request to the Flask server, which will get a particular exchange rate whenever it is asked for. JavaScript can then be used to update the DOM to render the new content.
- Here’s the interesting part of
application.py. There’s not much different here from last week, but note that what’s being returned is not a new webpage, but rather just a JSON object.@app.route("/convert", methods=["POST"]) def convert(): # Query for currency exchange rate currency = request.form.get("currency") res = requests.get("https://api.fixer.io/latest", params={ "base": "USD", "symbols": currency}) # Make sure request succeeded if res.status_code != 200: return jsonify({"success": False}) # Make sure currency is in response data = res.json() if currency not in data["rates"]: return jsonify({"success": False}) return jsonify({"success": True, "rate": data["rates"][currency]}) - The HTML is simply a basic form. The JavaScript code is in a different file, but linked in the
head.<html> <head> src=""> <title>Currency Converter</title> </head> <body> <form id="form"> <input id="currency" autocomplete="off" autofocus placeholder="Currency" type="text"> <input type="submit" value="Get Exchange Rate"> </form> <br>id=“result”></body> </html>url_for('static', filename='index.js')is Flask’s way of incorporating.jsfiles.staticis a separate folder.- The
resultdivwill contain the conversion, but is currently blank.
- The interesting code is inside of
index.js.document.addEventListener('DOMContentLoaded', () => { document.querySelector('#form').onsubmit = () => { // Initialize new request const request = new XMLHttpRequest(); const currency = document.querySelector('#currency').value; request.open('POST', '/convert'); // Callback function for when request completes request.onload = () => { // Extract JSON data from request const data = JSON.parse(request.responseText); // Update the result div if (data.success) { const contents = `1 USD is equal to ${data.rate} ${currency}.` document.querySelector('#result').innerHTML = contents; } else { document.querySelector('#result').innerHTML = 'There was an error.'; } } // Add data to send with request const data = new FormData(); data.append('currency', currency); // Send request request.send(data); return false; }; });- An
XMLHttpRequestis just an object that will allow an Ajax request to be made. request.openis where the new request is actually initialized, with the HTTP method and route being specified.JSON.parseconverts the raw response (request.responseText) into an object that can be indexed by keys and values.- The rest of the callback simply updates the HTML using template literals to reflect the result of the conversion.
FormDatais just an object that holds whatever the user input is.
- An
todo: websockets
Websockets
- The request-response model, which has been the basis for how HTTP requests and client-server interaction has been discussed so far, is useful as long as data is only being passed when a request is made. But, with ‘full-duplex communication’, more simply described as real-time communication, there is (or shouldn’t be) a need for reloading a webpage and making a new request just to check, for example, if someone sent a message in a chat room. Websockets are a protocol that allow for this type of communication, and Socket.IO is a particular JavaScript library that supports this protocol.
- This example will be based around a voting application that will count and display votes in real-time. Here’s the full
application.py, with all the setup and import statements.import os import requests from flask import Flask, jsonify, render_template, request from flask_socketio import SocketIO, emit app = Flask(__name__) app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") socketio = SocketIO(app) @app.route("/") def index(): return render_template("index.html") @socketio.on("submit vote") def vote(data): selection = data["selection"] emit("announce vote", {"selection": selection}, broadcast=True)flask_socketiois a library that allows for websockets inside a Flask application. This library allows for the web server and client to be emitting events to all other users, while also listening for and receiving events being broadcasted by others.submit voteis an event that will be broadcasted whenever a vote is submitted. The code for this will be in JavaScript.- Once a vote is received, the vote is announced to all users (
broadcast=True) with theemitfunction.
index.html:<html> <head> type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.6/socket.io.min.js"> src=""> <title>Vote</title> </head> <body> <ul id="votes"> </ul> <hr> <button data-vote="yes">Yes</button> <button data-vote="no">No</button> <button data-vote="maybe">Maybe</button> </body> </html>index.js:document.addEventListener('DOMContentLoaded', () => { // Connect to websocket var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port); // When connected, configure buttons socket.on('connect', () => { // Each button should emit a "submit vote" event document.querySelectorAll('button').forEach(button => { button.onclick = () => { const selection = button.dataset.vote; socket.emit('submit vote', {'selection': selection}); }; }); }); // When a new vote is announced, add to the unordered list socket.on('announce vote', data => { const li = document.createElement('li'); li.innerHTML = `Vote recorded: ${data.selection}`; document.querySelector('#votes').append(li); }); });- First, the websocket connection is established using a standard line to connect to wherever the application is currently running at.
submit voteis the name of the event that’s being submitted on a button click. That event just sends whatever the vote was.announce voteis an event received from the Python sever, which triggers the updating of the vote list.
- An improvement to this application would be to display a total vote count, instead of just listing every individual vote, and making sure that new users can see past votes.
- Changes to
application.py:votes = {"yes": 0, "no": 0, "maybe": 0} @app.route("/") def index(): return render_template("index.html", votes=votes) @socketio.on("submit vote") def vote(data): selection = data["selection"] votes[selection] += 1 emit("vote totals", votes, broadcast=True)- Now, any vote submissions are first used to update the
votesdictionary to keep a record of vote totals. Then, that entire dictionary is broadcasted.
- Now, any vote submissions are first used to update the
- Changes to
index.html:<body>Yes Votes: id="yes">No Votes: id=“no”>
Maybe Votes: id=“maybe”>
<hr> <button data-vote=“yes”>Yes</button> <button data-vote=“no”>No</button> <button data-vote=“maybe”>Maybe</button> </body>
- The
spanelements allocate a space for vote tallies to be filled in later.
- The
- Changes to
index.js:socket.on('vote totals', data => { document.querySelector('#yes').innerHTML = data.yes; document.querySelector('#no').innerHTML = data.no; document.querySelector('#maybe').innerHTML = data.maybe; });