I recently have been working on loading a local JSON file parsing it and displaying it using Javascript and I think I got it to where I really like it. I would like to know if anyone knows if there is a "purer" way of doing this using just Javascript without libraries.
<!DOCTYPE html>
<meta charset="UTF-8" />
<input type="file" id="files" name="files" accept=".json"/>
<output id="list"></output>
<div id="traveler_num"></div>
<div id="first_name"></div>
<script>
//stores the output of a parsed JSON file
const parsed = jsonText => JSON.parse(jsonText);
//creates a new file reader object
const fr = new FileReader();
function writeInfo (data) {
//modifies the DOM by writing info into different elements
document.getElementById('traveler_num').innerHTML = 'Traveler: ' + data.traveler_num;
document.getElementById('first_name').innerHTML = 'First Name: ' + data.first_name;
};
function handleFileSelect (evt) {
//function is called when input file is Selected
//calls FileReader object with file
fr.readAsText(evt.target.files[0])
};
fr.onload = e => {
//fuction runs when file is fully loaded
//parses file then makes a call to writeInfo to display info on page
writeInfo(parsed(e.target.result));
};
//event listener for file input
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>
-
1\$\begingroup\$ To prevent racing i would move the FileReader instantiation and the onload handler inside handleFileSelect. However the whole thing looks fine, good job ;) \$\endgroup\$Jonas Wilms– Jonas Wilms2017年12月18日 06:25:35 +00:00Commented Dec 18, 2017 at 6:25
-
2\$\begingroup\$ You already did it fine without any libraries? \$\endgroup\$Bergi– Bergi2017年12月18日 06:29:37 +00:00Commented Dec 18, 2017 at 6:29
-
\$\begingroup\$ Seems fine, but relying on the file's extension is a poor move IMO. You should at least wrap the JSON.parse in a try catch block. \$\endgroup\$Kaiido– Kaiido2017年12月18日 07:35:37 +00:00Commented Dec 18, 2017 at 7:35
4 Answers 4
handleFileSelect
is an Impure Function
handleFileSelect
has in your code snippet a dependence to fr
which is decleraded outside of the function. To make the function pure you can pass fr
as argument.
// impure..
function handleFileSelect (evt) {
fr.readAsText(evt.target.files[0])
}
// pure
function handleFileSelect (fr, evt) {
fr.readAsText(evt.target.files[0])
}
Put Side Effects Into a Wrapper
A pure function returns for the same input always the same output.
The function writeInfo
contains side effects because we access the DOM. We could wrap the DOM functions in to a wrapper called IO
which will always return a function - always the same function.
const IO = x => ({
map: (fn) => IO(x(fn)),
run: () => x()
})
// more about compose and the benefits on
// https://medium.com/javascript-scene/composing-software-an-introduction-27b72500d6ea
const compose = (f, g) => x => f(g(x))
const getElementById = id => IO(() => document.getElementById(id))
const writeInnerHTML = value => element => IO(() => element.run().innerHTML = value)
const selectAndWrite = value => compose(
writeInnerHTML(value),
getElementById
)
const writeInfo = data => IO(() => {
selectAndWrite('Traveler: ' + data.traveler_num)('traveler_num').run()
selectAndWrite('First Name: ' + data.first_name)('first_name').run()
})
// ONLY this call is impure
writeInfo({
traveler_num: 500,
first_name: 'Steven'
}).run()
<ul>
<li id="traveler_num"></li>
<li id="first_name"></li>
<ul>
Either You Can Parse to a JSON or Not..
In the following I want to introduce a functor calles Either which is split into to functors Left
and Right
. If we can parse a string succesfully we will return a Right
with the parsed value otherwise we will return Left
with an error message.
const Left = x => ({
map: (fn) => Left(x),
get: () => x
})
const Right = x => ({
map: (fn) => Right(fn(x)),
get: () => x
})
const tryFunction = (errorMessage, fn) => x => {
try {
return Right(fn(x))
} catch (error) {
return Left(errorMessage)
}
}
const parseToJson = tryFunction('Can\'t pase to JSON', JSON.parse)
console.log('should be {"name": "Random Name"}:', parseToJson('{"name": "Random Name"}').get())
console.log('should be "Can\'t pase to JSON":', parseToJson('{name: Random Name}').get())
References
The free online book mostly-adequate-guide is very good
The series composing-software is a good way to start
-
\$\begingroup\$ This is fantastic! I am stumbling through most of the code, but from what I can tell this is exactly what I am looking for. Do you mind maybe going a bit more in-depth on the how/why of things. Or list a reference material and I can learn myself. Seriously this is exactly what I am looking for. Thanks. \$\endgroup\$Justin Gagnon– Justin Gagnon2017年12月18日 13:57:39 +00:00Commented Dec 18, 2017 at 13:57
-
\$\begingroup\$ I add two ressources \$\endgroup\$Roman– Roman2017年12月18日 14:11:32 +00:00Commented Dec 18, 2017 at 14:11
If by functional mean arrow functions, then this is the solution i came up with
<!DOCTYPE html>
<meta charset="UTF-8"/>
<input type="file" id="files" name="files" accept=".json"/>
<output id="list"></output>
<div id="traveler_num"></div>
<div id="first_name"></div>
<script>
(function () {
const fileReader = new FileReader();
const travelerNumElement = document.getElementById('traveler_num');
const firstNameElement = document.getElementById('first_name');
this.writeInfo = (data) => {
travelerNumElement.innerHTML = 'Traveler: ' + data.traveler_num;
firstNameElement.innerHTML = 'First Name: ' + data.first_name;
}
this.handleFileSelect = (event) => fileReader.readAsText(event.target.files[0]);
fileReader.onload = (event) => this.writeInfo(JSON.parse(event.target.result));
document.getElementById('files').addEventListener('change', handleFileSelect, false);
})();
</script>
Always wrap things in (function() { //code here })
when you are using ES5, because it prevents the global scope of being bloated with unnecessary variables.
You can change the const
to this
and write this
everywhere in the scope, but it gets a bit ugly, so that's why i didn't do it.
And lastly, don't use abbreviations, imagine if someone else has to read the code, and the person can't see where the variable is defined, then fr
could just as well be flightRadar
I have tried my best to convert your code based on functional programming. But, I have achieved this much only without using any external library. Obviously, we can extract helper functions
from any fp
library like Ramda
but I guess that will be same as using any external library.
function attachEvent(id, event, fn) {
document.getElementById(id).addEventListener(event, fn, false);
}
function setInnerHTML(id, innerHTML) {
document.getElementById(id).innerHTML = innerHTML;
}
function readFile(file) {
return new Promise(resolve => {
const fr = new FileReader();
fr.onload = e => {
resolve(e.target.result);
};
fr.readAsText(file)
})
}
function handleFileSelect(e) {
readFile(e.target.files[0]).then(c => JSON.parse(c)).then(function (obj) {
setInnerHTML('traveler_num', 'Traveler: '.concat(obj.traveler_num));
setInnerHTML('first_name', 'First Name: '.concat(obj.first_name));
})
}
attachEvent('files', 'change', handleFileSelect);
<input type="file" id="files" name="files" accept=".json"/>
<div id="traveler_num"></div>
<div id="first_name"></div>
Really, your code is fine. These are mostly just nitpicks. You're already using vanilla JS with no (non-native) libraries.
- Your HTML has a doctype but no
<html>
or<head>
or<body>
... hopefully that's just for display and not your actual code. - Wrap the whole thing in an IIFE so everything is not in the global scope.
- No reason to store the
handleFileSelect
in memory since it's only used once. Use an anonymous or arrow function instead. - Put your
fr.onload
handler below thefr
"constant" so similar code is grouped. Remove the curlies and make it a one-liner. - The
accept
attribute is supposed to be a comma separated list of mime types, not the extension. However, at one point they did suggest that you also include the extension for better browser support. I remember that from a project I was working on but the page where it recommended that is now a dead link. - Your JS should be in it's own file, not inline.
(() => {
const fr = new FileReader();
fr.onload = e => writeInfo(parsed(e.target.result));
const parsed = jsonText => JSON.parse(jsonText);
function writeInfo(data) {
//modifies the DOM by writing info into different elements
document.getElementById('traveler_num').innerHTML = 'Traveler: ' + data.traveler_num;
document.getElementById('first_name').innerHTML = 'First Name: ' + data.first_name;
};
document.getElementById('files').addEventListener('change', e => fr.readAsText(e.target.files[0]), false);
})();
<input type="file" id="files" name="files" accept="application/json, .json" />
<output id="list"></output>
<div id="traveler_num"></div>
<div id="first_name"></div>
-
\$\begingroup\$ Thank you. I wrote everything together with the intention of separating out the JS into it's own file. I like the idea of wrapping everything in an IIFE, still have to get my head around all that. Removing the handleFileSelect would have never crossed my mind, that was great. I am still very new to both javascript and functional programming so I am trying to push myself. \$\endgroup\$Justin Gagnon– Justin Gagnon2017年12月18日 14:13:38 +00:00Commented Dec 18, 2017 at 14:13
-
\$\begingroup\$ @JustinGagnon - doing pretty good for a newbie. pls don't forget to hit the check mark next to your favorite answer - you can also upvote other answers that you liked as well. \$\endgroup\$I wrestled a bear once.– I wrestled a bear once.2017年12月18日 21:25:06 +00:00Commented Dec 18, 2017 at 21:25
-
\$\begingroup\$ I have upvoted but due to my low reputation it doesn't really do anything. \$\endgroup\$Justin Gagnon– Justin Gagnon2017年12月19日 02:18:29 +00:00Commented Dec 19, 2017 at 2:18