Tutorial 5: JavaScript and AJAX
Platform-Based Programming (CSGE602022) — Organized by the Faculty of Computer Science Universitas Indonesia, Odd Semester 2024/2025
Learning Objectives
After completing this tutorial, students are expected to be able to:
- Understand the usage of JavaScript in the context of front-end development
- Understand the basics of JavaScript
- Apply AJAX and Fetch API safely
JavaScript
Introduction to JavaScript
JavaScript is a high-level, multi-paradigm programming language. The multi-paradigm feature of JavaScript allows it to support object-oriented programming, imperative programming, and functional programming. JavaScript itself is an implementation of ECMAScript, that serves as a baseline for the JavaScript language. Other implementations of ECMAScript that is similar to JavaScript are JScript (Microsoft) and ActionScript (Adobe).
JavaScript, with HTML and CSS, is one of the three main technologies used in web development. The benefit of using JavaScript in web development is that dynamic page manipulation can be done and interaction between web pages and users can be increased. That is why most modern websites are using JavaScript on their web page to give the best experience to the user. Some things that we could do with JavaScript are to display information based on time, recognizing the type of device used, do validation on forms or data, creating cookies (not literally, but HTTP cookies), change the CSS of an element dynamically, etc.
Usually, JavaScript is used on the client-side of a web (client-side JavaScript), but there are some types of JavaScript that are used on the server-side of a web (server-side JavaScript) like node.js. Client-side means that the JavaScript code will be executed on the user's system, not on the website's server. This means that the JavaScript code complexity will not affect the server's performance but will affect the user's system performance; where more complex JavaScript will force the browser to use more memory.
In the PBD course, we will only focus on the client-side JavaScript.
JavaScript Execution by Browser
Take a look at the diagram below to understand how JavaScript execution is done by the browser.
After the browser downloads the HTML web page, the browser will look for the <script></script>
tag, and the browser will see the tag script if the tag contains embedded JavaScript or external JavaScript. If the script tag refers to an external JavaScript code, the browser will download that file first.
JavaScript Writing
JavaScript could be added as an embedded JavaScript or external JavaScript. JavaScript code could be defined or written as an embedded code in an HTML file or as a separate file. If written outside of the HTML, the file extension for the JavaScript file is .js
. Below is an example of how to define a JavaScript code.
JavaScript could be placed in the head or body of an HTML file. In addition, the JavaScript code must be placed between the <script>
and </script>
tags. You can place more than one script tag containing JavaScript code in an HTML file.
Embedded JavaScript pada HTML
<script type="text/JavaScript">
alert("Hello World!");
</script>
External JavaScript pada HTML
<script type="text/JavaScript" src="js/script.js"></script>
alert("Hello World!");
On the external JavaScript file, the <script>
tag is no longer needed.
Separating JavaScript from the external file can provide several benefits like the code can be used in other HTML files, JavaScript and HTML do not conflict, so the focus is on developing the application, and the page loading speed can be improved. .js
files are usually cached by the browser so that if we open the page again and there is no changes on the .js
file, then the browser will not request the file to the server again but it will use the cached file that has been stored.
JavaScript Execution
After the JavaScript code has been loaded, then the browser will start executing the JavaScript code. If the code is not event-triggered, then it will be executed immediately. If the code is event-triggered, then it will only be executed if the defined event is triggered.
// immediately executed
alert("Hello World");
// immediately executed
var obj = document.getElementById("object");
// immediately executed, adding an event handler for the onclick event to the object
obj.onclick = function () {
// only executed if the 'object' element is clicked
alert("You just clicked the object!");
};
JavaScript Syntax
Variables
Defining variables in JavaScript is very easy. Here is an example.
var example = 0; // var example is a number
var example = "example"; // var example is a string
var example = true; // var example is a boolean
JavaScript can hold many types of data; from strings, integers, even objects. Unlike Java, which uses data types, JavaScript has a loosely typed or dynamic language, meaning that you do not need to specify the type of data in the head variable (for example, if you want to create a variable with type data int
, the syntax would be int x = 9
), JavaScript will automatically read the type of data based on the standard (as shown in the example above).
There are some rules in choosing the identifiers or variable names in JavaScript. The first character must be an alphabet, underscore (_
), or dollar sign ($
). In addition, JavaScript identifiers are case sensitive.
String Concatenation
In JavaScript, we can also concatenate string
with string
like in Java.
var str1 = "PBP" + " " + "Fun";
var str2 = "PBP";
var str3 = "Fun";
var str4 = str2 + " " + str3;
var str5 = "Fun";
var str6 = `PBP ${str5}`; // Has the same result as "PBP" + " " + str5
JavaScript Scopes
Local Variables
Variables defined within a function are scoped locally, so only the code within the function can access the variable.
// code outside the thisFunction() function cannot access the courseName variable
function thisFunction() {
var courseName = "PBP";
// code within the thisFunction() function can access the courseName variable
}
Global Variables
Variables defined outside a function are global and can be accessed by other JavaScript code in the same file.
var courseName = "PBP";
function thisFunction() {
// code within the thisFunction() function can access the courseName variable
}
Auto Global Variables
Value assigned to a variable that has not been declared automatically becomes a global variable, even though the variable is within a function.
thisFunction(); // thisFunction() function must be called first
console.log(courseName); // print "PBP" to the JavaScript console
function thisFunction() {
courseName = "PBP";
}
Accessing Global Variables from HTML
You can access variables that are in JavaScript files in HTML files that load the JavaScript file.
...
<input type="text" onclick="this.value=courseName" />
...
...
var courseName = "PBP";
...
Function and Event
Function is a group of code that can be called anywhere in the code program (similar to method
in Java). This reduces redundancy of code (reducing code that can be repeated). Other than that, functions in JavaScript are useful to dynamically invoke elements. Functions can be called by another function or triggered by an event (which will be explained below). Below is an example of an index.html
file.
...
<input type="button" value="magicButton" id="magicButton" onclick="hooray();" />
...
Here is the code in javascript.js
.
...
function hooray() {
alert("Yahoo!");
}
...
If magicButton
is pressed, then the onclick
function will run the hooray()
function in javascript.js
, then the alert will be displayed according to what was previously assigned.
The onclick
code is an example of JavaScript's event. An event is JavaScript's ability to create a dynamic website. The meaning of onclick
is to indicate what will happen to JavaScript if the element is pressed. Other than that, events are usually associated with a function that could be used as commands in JavaScript. There are also other examples of events such as onchange
, onmouseover
, onmouseout
, and others, that you can read more here.
JavaScript DOM
HTML DOM
HTML DOM (Document Object Model) is a standard of how to change, take, and delete HTML elements. HTML DOM can be accessed through JavaScript or other programming languages. The details can be found here.
Here is an implementation example.
...
<div>
<p onclick="myFunction()" id="demo">Example of HTML DOM</p>
</div>
...
...
function myFunction() {
document.getElementById("demo").innerHTML = "YOU CLICKED ME!";
}
...
CSS DOM
Similar to HTML DOM, CSS DOM can change CSS dynamically through JavaScript. The details can be found here.
Here is an implementation example.
...
<p id="blueText" onclick="changeColor()">Click me v2</p>
...
...
function changeColor(){
document.getElementById("blueText").style.color="blue";
}
...
AJAX
Introduction to AJAX
AJAX is an abbreviation of Asynchronous JavaScript And XML.
AJAX is not a programming language, but a technology that provides the browser (to request data from the web server) with JavaScript and HTML DOM (to display data). AJAX can use XML to send data, but AJAX can also use text or JSON to send data. AJAX allows a web page to fetch data asynchronously by sending data to the server in the background. This means that we can update parts of a page without having to refresh the entire page.
Here is a diagram of how AJAX works.
- An event occurs on the web page (for example, the submit data button is pressed)
- A
XMLHttpRequest
object is created by JavaScript - The
XMLHttpRequest
object sends the request to the server - The server processes the request
- The server returns the response back to the web page
- The response is read by JavaScript
- The next action will be triggered by JavaScript according to the step that was made (for example, update the data on the page)
XMLHttpRequest
was previously a standard way to make an AJAX request in JavaScript. However, XMLHttpRequest has some drawbacks, such as cumbersome handling when working with promises and callbacks, and limited support for modern code patterns.
Therefore, fetch()
was introduced as a new API for making HTTP requests with a simpler syntax and direct support for promises. This allows developers to write code that is easier to read, manage, and better aligned with modern asynchronous paradigms like async/await. fetch()
is also more flexible in handling other data formats such as JSON, and provides a better API for handling errors and HTTP responses. Further explanation of the differences between fetch
and XMLHttpRequest
can be found here.
On this course, you will be performing AJAX on a web browser using the fetch()
function provided by JavaScript. In general, the use of fetch()
for making AJAX calls can be seen here.
Fetch API
Fetch API is a new API that was introduced in ECMAScript 2020 as a new standard for making requests with Promise
. Fetch API provides an interface for obtaining data sources (including all networks). It is a more flexible and powerful replacement of XMLHttpRequest
. The Fetch API generally used to implement AJAX more easily than with XMLHttpRequest
. Additionally, the Fetch API supports more HTTP methods and headers compared to traditional AJAX.
The fetch()
function has several parameters, including:
url
: The URL of the data source that will be requestedmethod
: The HTTP method that will be usedheaders
: The HTTP headers that will be sentbody
: The content of the request
The fetch()
function returns an Response
object. The Response
object has several properties, including:
status
: Thestatus
code of the responseheaders
: The HTTP headers of the responsebody
: The content of the response
You can learn more about the Fetch API here.
Async and Await Functions
Before learning how to use the fetch()
function, it's recommended that we first learn about the async
and await
functions which allow us to implement AJAX without the need to use an external library, such as jQuery.
The async
and await
functions are new functions introduced in ECMAScript 2017. The async
function is used to indicate a function as an asynchronous function, while the await
function is used to wait for the result of an async
function.
You can learn more about the async
and await
functions at this link.
Using Fetch API
The Fetch API provides a JavaScript interface to access and manipulate parts of the protocol, such as requests and responses. The API also provides the fetch()
global method that provides an easy and reliable way to fetch data asynchronously across the entire network.
Unlike XMLHttpRequest
, which is based on callbacks, Fetch API is based on Promise
and provides an alternative that is easier and more reliable to use in service worker. This API also integrates advanced HTTP concepts such as CORS and other extensions to HTTP.
Here is an example of using Fetch API with the async
and await
functions to perform AJAX.
async function fetchData() {
const response = await fetch("https://hp-api.onrender.com/api/characters");
const data = await response.json();
return data;
}
const characters = await fetchData();
console.log(characters);
The code above will perform an AJAX request to request data from the Harry Potter characters API asynchronously. The AJAX request will store the result in the characters
variable.
You can learn more about the Fetch API at this link.
Tutorial: Adding Error Message to Login
To make the login process easier for your users, provide a conditional to the login_user
view in your application as such.
...
if form.is_valid():
user = form.get_user()
login(request, user)
response = HttpResponseRedirect(reverse("main:show_main"))
response.set_cookie('last_login', str(datetime.datetime.now()))
return response
else:
messages.error(request, "Invalid username or password. Please try again.")
...
Code Explanation:
messages.error(request, msg)
will "render" an error message to therequest
that is sending the login request, which will then be displayed in thelogin.html
template.
Tutorial: Creating Function to Add a Mood with AJAX
In this section, you will create a function in the views to add a new mood to the data base with AJAX.
-
Add the following two imports to the
views.py
file.main/views.pyfrom django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST -
Create a new function in the
views.py
file with the nameadd_mood_entry_ajax
that receives therequest
parameter as follows.main/views.py...
@csrf_exempt
@require_POST
def add_mood_entry_ajax(request):
mood = request.POST.get("mood")
feelings = request.POST.get("feelings")
mood_intensity = request.POST.get("mood_intensity")
user = request.user
new_mood = MoodEntry(
mood=mood, feelings=feelings,
mood_intensity=mood_intensity,
user=user
)
new_mood.save()
return HttpResponse(b"CREATED", status=201)
...Code Explanation:
- The
csrf_exempt
decorator tells Django to not check thecsrf_token
in thePOST
request that is sent to the function. - The
require_POST
decorator makes the function only accessible when the user sends aPOST
request to the function. If the user sends a request with a method other than that, then a405 Method Not Allowed
error will be returned. mood = request.POST.get("mood")
and similarly used to retrieve data sent by the user through thePOST
request manually.new_mood = MoodEntry(...)
is an objectMood
new that is created manually based on the datas sent from thePOST
request.
- The
Tutorial: Add Routing For add_mood_entry_ajax
-
Open
urls.py
on themain
subdirectory and import the function that you have already made.main/urls.pyfrom main.views import ..., add_mood_entry_ajax
-
Add the URL path inside of
urlpatterns
to allow access to the functions that has been imported.main/urls.pyurlpatterns = [
...
path('create-mood-entry-ajax', add_mood_entry_ajax, name='add_mood_entry_ajax'),
]
Tutorial: Displaying Mood Entry Data with fetch()
API
-
Open the
views.py
file and remove the two lines below.main/views.pymood_entries = MoodEntry.objects.filter(user=request.user)
and
main/views.py'mood_entries': mood_entries,
In this tutorial, we will fetch the Mood Entry objects from the
/json
endpoint, therefore the code above is no longer required. -
Open the
views.py
file and change the first line of theshow_json
andshow_xml
functions as follows.main/views.pydata = MoodEntry.objects.filter(user=request.user)
-
Open the
main.html
file and remove themood_entries
block conditional to display the card_mood when empty. This is the code snippet that you have to remove.main/templates/main.html...
{% if not mood_entries %}
<div class="flex flex-col items-center justify-center min-h-[24rem] p-6">
<img src="{% static 'image/sedih-banget.png' %}" alt="Sad face" class="w-32 h-32 mb-4"/>
<p class="text-center text-gray-600 mt-4">No mood data on the mental health tracker yet</p>
</div>
{% else %}
<div class="columns-1 sm:columns-2 lg:columns-3 gap-6 space-y-6 w-full">
{% for mood_entry in mood_entries %}
{% include 'card_mood.html' with mood_entry=mood_entry %}
{% endfor %}
</div>
{% endif %}
...After removing the code snippet above, add this code snippet on the same place.
main/templates/main.html<div id="mood_entry_cards"></div>
-
Create a
<script>
block below the file (before{% endblock content %}
) and create a new function in the<script>
block with the namegetMoodEntries
.main/templates/main.html<script>
async function getMoodEntries(){
return fetch("{% url 'main:show_json' %}").then((res) => res.json())
}
</script>Code Explanation:
- This function uses the
fetch()
API to fetch the JSON data asynchronously. - After the data is fetched, the
then()
function is used to parse the JSON data into a JavaScript object.
- This function uses the
-
Create a new function on the
<script>
block with the namerefreshMoodEntries
that is used to refresh moods data asynchronously.main/templates/main.html<script>
...
async function refreshMoodEntries() {
document.getElementById("mood_entry_cards").innerHTML = "";
document.getElementById("mood_entry_cards").className = "";
const moodEntries = await getMoodEntries();
let htmlString = "";
let classNameString = "";
if (moodEntries.length === 0) {
classNameString = "flex flex-col items-center justify-center min-h-[24rem] p-6";
htmlString = `
<div class="flex flex-col items-center justify-center min-h-[24rem] p-6">
<img src="{% static 'image/sedih-banget.png' %}" alt="Sad face" class="w-32 h-32 mb-4"/>
<p class="text-center text-gray-600 mt-4">No mood data on the mental health tracker yet.</p>
</div>
`;
}
else {
classNameString = "columns-1 sm:columns-2 lg:columns-3 gap-6 space-y-6 w-full"
moodEntries.forEach((item) => {
htmlString += `
<div class="relative break-inside-avoid">
<div class="absolute top-2 z-10 left-1/2 -translate-x-1/2 flex items-center -space-x-2">
<div class="w-[3rem] h-8 bg-gray-200 rounded-md opacity-80 -rotate-90"></div>
<div class="w-[3rem] h-8 bg-gray-200 rounded-md opacity-80 -rotate-90"></div>
</div>
<div class="relative top-5 bg-indigo-100 shadow-md rounded-lg mb-6 break-inside-avoid flex flex-col border-2 border-indigo-300 transform rotate-1 hover:rotate-0 transition-transform duration-300">
<div class="bg-indigo-200 text-gray-800 p-4 rounded-t-lg border-b-2 border-indigo-300">
<h3 class="font-bold text-xl mb-2">${item.fields.mood}</h3>
<p class="text-gray-600">${item.fields.time}</p>
</div>
<div class="p-4">
<p class="font-semibold text-lg mb-2">My Feeling</p>
<p class="text-gray-700 mb-2">
<span class="bg-[linear-gradient(to_bottom,transparent_0%,transparent_calc(100%_-_1px),#CDC1FF_calc(100%_-_1px))] bg-[length:100%_1.5rem] pb-1">${item.fields.feelings}</span>
</p>
<div class="mt-4">
<p class="text-gray-700 font-semibold mb-2">Intensity</p>
<div class="relative pt-1">
<div class="flex mb-2 items-center justify-between">
<div>
<span class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-indigo-600 bg-indigo-200">
${item.fields.mood_intensity > 10 ? '10+' : item.fields.mood_intensity}
</span>
</div>
</div>
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-indigo-200">
<div style="width: ${item.fields.mood_intensity > 10 ? 100 : item.fields.mood_intensity * 10}%;" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-indigo-500"></div>
</div>
</div>
</div>
</div>
</div>
<div class="absolute top-0 -right-4 flex space-x-1">
<a href="/edit-mood/${item.pk}" class="bg-yellow-500 hover:bg-yellow-600 text-white rounded-full p-2 transition duration-300 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-9 w-9" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</a>
<a href="/delete/${item.pk}" class="bg-red-500 hover:bg-red-600 text-white rounded-full p-2 transition duration-300 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-9 w-9" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
`;
});
}
document.getElementById("mood_entry_cards").className = classNameString;
document.getElementById("mood_entry_cards").innerHTML = htmlString;
}
refreshMoodEntries();
</script>Code Explanation:
document.getElementById("mood_entry_cards")
is used to get the element based on its ID. In this code, the element that is being targeted is the<div>
tag with the IDmood_entry_cards
that you have created previously.innerHTML
is used to fill in the child element of the element that is being targeted. IfinnerHTML = ""
, then it will clear the contents of the child element of the element that is being targeted.className
is used to fill in the class name of the element that is being targeted.moodEntries.forEach((item))
is used to perform a for each loop on the data moods that is retrieved using thegetMoodEntries()
function. Then,htmlString
will be concatenated with the data moods to display in the container with the cards like in the previous tutorial.refreshMoodEntries()
is used to call the function above when the page is loaded.
Tutorial: Creating Modal as a Form to Add a Mood
-
Add the following code to implement the modal (Tailwind) on your application. You can place the following code below the
div
with theid
mood_entry_cards
that you have added previously.main/templates/main.html...
<div id="crudModal" tabindex="-1" aria-hidden="true" class="hidden fixed inset-0 z-50 w-full flex items-center justify-center bg-gray-800 bg-opacity-50 overflow-x-hidden overflow-y-auto transition-opacity duration-300 ease-out">
<div id="crudModalContent" class="relative bg-white rounded-lg shadow-lg w-5/6 sm:w-3/4 md:w-1/2 lg:w-1/3 mx-4 sm:mx-0 transform scale-95 opacity-0 transition-transform transition-opacity duration-300 ease-out">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 border-b rounded-t">
<h3 class="text-xl font-semibold text-gray-900">
Add New Mood Entry
</h3>
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" id="closeModalBtn">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="px-6 py-4 space-y-6 form-style">
<form id="moodEntryForm">
<div class="mb-4">
<label for="mood" class="block text-sm font-medium text-gray-700">Mood</label>
<input type="text" id="mood" name="mood" class="mt-1 block w-full border border-gray-300 rounded-md p-2 hover:border-indigo-700" placeholder="Enter your mood" required>
</div>
<div class="mb-4">
<label for="feelings" class="block text-sm font-medium text-gray-700">Feelings</label>
<textarea id="feelings" name="feelings" rows="3" class="mt-1 block w-full h-52 resize-none border border-gray-300 rounded-md p-2 hover:border-indigo-700" placeholder="Describe your feelings" required></textarea>
</div>
<div class="mb-4">
<label for="moodIntensity" class="block text-sm font-medium text-gray-700">Mood Intensity (1-10)</label>
<input type="number" id="moodIntensity" name="mood_intensity" min="1" max="10" class="mt-1 block w-full border border-gray-300 rounded-md p-2 hover:border-indigo-700" required>
</div>
</form>
</div>
<!-- Modal footer -->
<div class="flex flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-2 p-6 border-t border-gray-200 rounded-b justify-center md:justify-end">
<button type="button" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg" id="cancelButton">Cancel</button>
<button type="submit" id="submitMoodEntry" form="moodEntryForm" class="bg-indigo-700 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-lg">Save</button>
</div>
</div>
</div>
... -
Because we are using vanilla Tailwind CSS, there is no built-in modal class. Therefore, to make the modal work, we need to add the following JavaScript functions.
main/templates/main.html<script>
...
const modal = document.getElementById('crudModal');
const modalContent = document.getElementById('crudModalContent');
function showModal() {
const modal = document.getElementById('crudModal');
const modalContent = document.getElementById('crudModalContent');
modal.classList.remove('hidden');
setTimeout(() => {
modalContent.classList.remove('opacity-0', 'scale-95');
modalContent.classList.add('opacity-100', 'scale-100');
}, 50);
}
function hideModal() {
const modal = document.getElementById('crudModal');
const modalContent = document.getElementById('crudModalContent');
modalContent.classList.remove('opacity-100', 'scale-100');
modalContent.classList.add('opacity-0', 'scale-95');
setTimeout(() => {
modal.classList.add('hidden');
}, 150);
}
document.getElementById("cancelButton").addEventListener("click", hideModal);
document.getElementById("closeModalBtn").addEventListener("click", hideModal);
...
</script>infoThe form and JavaScript functions in the modal that we have created are adapted to the model in the
mental_health_tracker
application. If you want to use other models with other fields, pay attention again to the values of the related HTML attributes. -
Change the Add New Mood Entry button that you have added in the tutorial above and add a new button to perform the addition of data with AJAX.
main/templates/main.html...
<a href="{% url 'main:create_mood_entry' %}" class="bg-indigo-400 hover:bg-indigo-400 text-white font-bold py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105 mx-4 ">
Add New Mood Entry
</a>
<button data-modal-target="crudModal" data-modal-toggle="crudModal" class="btn bg-indigo-700 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:-translate-y-1 hover:scale-105" onclick="showModal();">
Add New Mood Entry by AJAX
</button>
...
Tutorial: Adding Data Mood with AJAX
The modal with the form that you have created previously cannot be used to add data mood. Therefore, you need to create a new JavaScript function to add data based on the input to the data base in an AJAX.
-
Create a new function in the block
<script>
with the nameaddMoodEntry
.main/templates/main.html<script>
function addMoodEntry() {
fetch("{% url 'main:add_mood_entry_ajax' %}", {
method: "POST",
body: new FormData(document.querySelector('#moodEntryForm')),
})
.then(response => refreshMoodEntries())
document.getElementById("moodEntryForm").reset();
document.querySelector("[data-modal-toggle='crudModal']").click();
return false;
}
...
</script>Code Explanation:
new FormData(document.querySelector('#moodEntryForm'))
is used to create a newFormData
object that contains the data from the form in the modal. TheFormData
object can be used to send the form data to the server.document.getElementById("moodEntryForm").reset()
is used to clear the contents of the field form modal after submitting.
-
Add an event listener to the form in the modal to run the
addMoodEntry()
function with the following code.main/templates/main.html<script>
...
document.getElementById("moodEntryForm").addEventListener("submit", (e) => {
e.preventDefault();
addMoodEntry();
})
</script>Code Explanation:
document.getElementById("moodEntryForm")
: Retrieves an element with the ID "moodEntryForm" from the DOM, which is the form element in the modal used for adding a mood entry using AJAX..addEventListener("submit", ...)
: Adds an event listener to the form that was selected from the DOM in the previous step, then attaches a callback function that will be invoked when the form is submitted (i.e., when the input element withtype="submit"
is clicked).(e) => {...
: The callback function provided to the event listener, which will be executed when the form is submitted (the function is written using the arrow function notation introduced in the ES6 standard of JavaScript; you can explore this concept further at about arrow functions).e.preventDefault()
: By default, a form that experiences a submit event will attempt to send data to the URL specified in theaction
attribute of theform
tag. However, since we are using a different AJAX method than what is typically done in Django, we want to disable this default behavior using thepreventDefault()
method.addMoodEntry()
: Calls the function to add a mood entry.
Congratulations! You have successfully created an application that can add data using AJAX. Go to http://localhost:8000/ and try to add a new mood entry to the application. Now, the application should not need a reload every time a new mood entry has been added.
Tutorial: Protecting the Application from Cross Site Scripting (XSS)
Trying XSS
Did you notice that the code that you have just added actually opens a security hole in the application? To see the severity of the security hole, follow the steps below.
-
Add a new mood entry with the value of the
mood
field as follows. Other fields can be filled in according to your preference.<img src=x onerror="alert('XSS!');">
-
Press the save button and if the storage is successful, you will get an alert with the value
XSS!
as shown in the figure below.
Adding strip_tags
to "Clean Up" New Data
From the input above, it can be seen that a hacker can add new data that can execute JavaScript in the user's browser application. The security hole is usually called Cross Site Scripting or XSS. More specifically, in this application, there is a Stored XSS, which is a payload for XSS that is stored in the application's data base. Of course, we do not want the application that we are creating to have a security hole. To close the security hole, you can follow the steps below.
-
Open the
views.py
andforms.py
files and add the following imports.main/views.py, main/forms.pyfrom django.utils.html import strip_tags
-
In the
add_mood_entry_ajax
function in theviews.py
file, use thestrip_tags
function on themood
andfeelings
data before the data is inserted into theMoodEntry
.main/views.py...
@csrf_exempt
@require_POST
def add_mood_entry_ajax(request):
mood = strip_tags(request.POST.get("mood")) # strip HTML tags!
feelings = strip_tags(request.POST.get("feelings")) # strip HTML tags!
...Code Explanation:
- The
strip_tags
function will remove all HTML tags that are present in the data sent by the user through thePOST
request, so that the data that is stored in the data base is clean. For example,data = "<b>Programming</b> <button>Basic</button> Platform <span>Genap 2025</span>"
,strip_tags(data)
will returnProgramming Basic Platform Genap 2025
. - The
mood_intensity
field is not cleaned up withstrip_tags
because the field is anIntegerField
and if it is not anint
, Django will not store it in the data base.
- The
-
On the
MoodEntryForm
class in theforms.py
file, add the following two methods.main/forms.py...
class MoodEntryForm(ModelForm):
class Meta:
...
def clean_mood(self):
mood = self.cleaned_data["mood"]
return strip_tags(mood)
def clean_feelings(self):
feelings = self.cleaned_data["feelings"]
return strip_tags(feelings)
...Code Explanation:
- The
clean_mood
andclean_feelings
methods will be called whenform.is_valid()
is called, so by adding the two methods, you have done validation for thecreate_mood_entry
andedit_mood
functions.
- The
-
After adding
strip_tags
, remove the data that you have just added and try to add it again. If you get an error on the form that says themood
field cannot be empty, then congratulations, you have added a security hole against XSS! If you do not get an error, check again whether you have followed the steps above.
Sanitizing Data with DOMPurify
The strip_tags
function that you have added will "clean up" all new data, but old data must be cleaned up itself or we can use a JavaScript library called DOMPurify for cleaning up on the frontend side. It is important to note that DOMPurify will only work if the data is retrieved to be displayed with HTML on the application's frontend. If there is an API /json
or /xml
used by the application, then the data that is obtained will still be "dirty".
-
Open the
main.html
file and add the following code to themeta
block.main/templates/main.html{% block meta %}
...
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js"></script>
...
{% endblock meta %} -
After that, add the following code to the
refreshMoodEntries
function that you have added previously.main/templates/main.html<script>
...
async function refreshMoodEntries() {
...
moodEntries.forEach((item) => {
const mood = DOMPurify.sanitize(item.fields.mood);
const feelings = DOMPurify.sanitize(item.fields.feelings);
...
});
...
}
...
</script>noteDon't forget to change all occurrences of
item.fields.mood
tomood
anditem.fields.feelings
tofeelings
. -
Refresh the main page and if you have previously had dirty data like the alert box that shows up, then the alert box should no longer appear on the browser.
Closing
- After running the tutorial above, you should have the local directory structure as follows.
mental-health-tracker
├── .github\workflows
│ └── push.yml
├── env
├── main
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0001_initial.py
│ │ ├── 0002_alter_moodentry_id.py
│ │ └── 0003_moodentry_user.py
│ ├── templates
│ │ ├── create_mood_entry.html
│ │ ├── card_info.html
│ │ ├── card_mood.html
│ │ ├── edit_mood.html
│ │ ├── login.html
│ │ ├── main.html
│ │ └── register.html
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── mental_health_tracker
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── templates
│ └── base.html
│ └── navbar.html
├── static
│ └── css
│ └── global.css
│ └── image
│ └── sedih-banget.png
├── .gitignore
├── manage.py
└── requirements.txt
Additional References
Contributors
- Juan Maxwell Tanaya
- Muhammad Faishal Adly Nelwan
- Muhammad Irfan Firmansyah
- Muhammad Oka (EN Translation)
Credits
This tutorial was developed based on PBP Odd 2024 and PBP Even 2024 written by the 2024 Platform-Based Programming Teaching Team. All tutorials and instructions included in this repository are designed so that students who are taking Platform-Based Programming courses can complete the tutorials during lab sessions.