In Part 2 of this series we built a simple File Manager in Python, using a browser tab to display its output. While this works well enough in a simple file manager it's unsuited for applications where the UI shows things that are not directly related to user actions, such as an on-screen clock or a news feed that updates without any user action needed. Conventional HTML pages are reactive but we may need our application to be proactive. Because the server cannot directly control the UI, we'll move part or all of the MVC Controller into the browser and implement it in JavaScript. (Edit: Since writing these articles I have become aware of web sockets. I hope to write a future article comparing and contrasting them with REST.)
In the previous version, all the HTML was created using templates on the server, with the finished pages being returned to the browser in response to user clicks. In this version, the only data returning from the server will be the lists of directories and files. All the display-related stuff will be kept in the browser.
As before, we start with an HTML file. Let's call it index.html
so it becomes the default page on the embedded webserver:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVC Demo</title>
<link rel='stylesheet' href='css/client.css' type='text/css' media='all' />
<script type='text/javascript' src='js/client.js'></script>
</head>
<body>
<h2 id="header"></h2>
<div id="panel"></div>
</body>
</html>
The header of this file calls for a CSS stylesheet and a JavaScript file. In the body are the header and the content panel as before, but now they are empty and their styles are called from the CSS file. Each element has an ID that enables JavaScript to find it easily.
The CSS
This is pretty basic:
h2 {
text-align:center;
}
div#panel {
background:#ffffee;
border:1px solid gray;
font-family:mono;
margin-left:auto;
margin-right:auto;
padding:0.5em;
width:90%;
}
The JavaScript
The file client,js
is where everything happens. The first thing we need is some AJAX code, to send requests to the server and handle responses. We also have a couple of constants; the URL of the server and the default path, here set to the root of the file system.
const HOST = `http://localhost:8080`;
const PATH = '';
////////////////////////////////////////////////////////////
// CONTROLLER
// Handle an AJAX request
function ajax(action, path, onload) {
const request = new XMLHttpRequest();
const url = `${HOST}${path}`;
console.log(`URL: ${url}`);
request.open(action, url, true);
if (!request) {
alert(`Can't open AJAX channel`);
return;
}
request.onload = onload;
request.onerror = function () {
alert(request.responseText);
};
request.send();
}
// Get a JSON-encoded listing of a directory.
function getFiles(path, display) {
ajax(`get`, `/listfiles${path}`, function (response) {
const lists = JSON.parse(response.target.responseText);
display(path, lists);
});
}
// List a file.
function listFile(path, display) {
ajax(`get`, `/readfile${path}`, function (response) {
const replaceAll = function(target, search, replacement) {
return target.split(search).join(replacement);
};
let content = replaceAll(response.target.responseText, '&', '&');
content = replaceAll(content, '<', '<');
content = replaceAll(content, '>', '>');
content = replaceAll(content, '%09', ' ');
content = replaceAll(content, ' ', ' ');
content = replaceAll(content, '%0a', '<br>');
display(path, content);
});
}
First we have the AJAX function itself. It takes the HTTP method (get
or post
), the full path containing the request and a callback function for when the request succeeded. This will be called with a response
object containing everything we need to know.
We then have 2 more functions; one gets a JSON-encoded listing of a directory, as described in Part 1, and the other gets the contents of a file given its path and a function it will call to do the actual display. It assumes the file will be shown in a browser so it encodes special characters to force them to be displayed as text rather than being interpreted as HTML.
All the above is Controller code as it does not relate to how the information is to be presented, only how to get it from the server. The rest of the code is all View:
////////////////////////////////////////////////////////////
// VIEW
// Display the list received from the server
function displayFiles(path, lists) {
const dirs = lists.dirs;
const files = lists.files;
const header = document.getElementById(`header`);
header.innerHTML = `List of files in ${path ? path : `/`}`;
const panel = document.getElementById('panel');
panel.innerHTML = ``;
dirs.forEach(function (name, index) {
displayFileName(`dir`, name, panel, path);
});
panel.appendChild(document.createElement(`br`));
files.forEach(function (name, index) {
displayFileName(`file`, name, panel, path);
});
}
// Display a file name, with a hyperlink if appropriate
function displayFileName(type, name, panel, path) {
if (name.startsWith(`.`)) {
return;
}
const element = document.createElement('div');
panel.appendChild(element);
x = name.lastIndexOf(`.`);
const extension = (x >= 0) ? name.substring(x) : -1;
const clickable = ![`.png`, `.jpg`, `.gif`, `.mp3`].includes(extension);
let link = null;
if (clickable) {
link = document.createElement('a');
link.setAttribute(`href`, `#`);
link.innerHTML = name;
element.appendChild(link);
} else {
element.innerHTML = name;
}
if (type == `dir`) {
doDirLink(link, path);
} else if (clickable) {
doFileLink(link, path);
}
}
// Handle a click on a directory name
function doDirLink(link, path) {
link.onclick = function (event) {
event.stopPropagation();
getFiles(`${path}/${this.innerText}`, displayFiles);
}
return false;
}
//Handle a click on a file name
function doFileLink(link, path) {
link.filePath = path;
link.onclick = function (event) {
event.stopPropagation();
listFile(`${this.filePath}/${this.innerText}`, function (path, content) {
const header = document.getElementById(`header`);
header.innerHTML = `Content of ${path}`;
const panel = document.getElementById('panel');
panel.innerHTML = content;
});
}
return false;
}
// Display the content of a file
function displayFile(path, content) {
const header = document.getElementById(`header`);
header.innerHTML = `Content of ${path}`;
const panel = document.getElementById('panel');
panel.innerHTML = content;
}
And finally, right at the bottom of the file, outside any function, JavaScript waits for an event that tells it the page is loaded. You can think of this as the main program, only executed once when the program starts. Its action is to request the contents of a directory.
window.onload = function () {
getFiles(PATH, displayFiles);
};
To fire up this version of the File Manager you can modify the launcher in the Python script:
cmd = f'chromium-browser {HOST()}'
or if the server is already running, open a new browser tab and go to
http://localhost:8080
This version of the embryonic File Manager behaves the same way as the previous one, that did everything in the Python server script. Apart from one difference, that is, being the behavior of the browser Back
button. On the server version it behaves normally, allowing you to back up to a previous step, but in this JavaScript version it throws you out completely. One of the (many) fiddly things to do when building a client-side application is to consider the Back
button, without which many of your users will not thank you. This whole area has to do with the management of the browser history, and I confess that even after several hours fiddling about I've been unable to get it to work. Since it's not really the point of this series I beg your indulgence. Of course, if anyone is minded to provide code that works I shall be delighted to rewrite this section and acknowledge the solution provider.
In Part 4 of this series I'll show a third alternative; another client-side solution that keeps the Python-based webserver but dispenses with JavaScript almost completely.
Photo by Markus Spiske on Unsplash
Top comments (0)