2
\$\begingroup\$

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>
\$\endgroup\$

migrated from stackoverflow.com Dec 18 '17 at 6:51

This question came from our site for professional and enthusiast programmers.

  • 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 Dec 18 '17 at 6:25
  • 2
    \$\begingroup\$ You already did it fine without any libraries? \$\endgroup\$ – Bergi Dec 18 '17 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 Dec 18 '17 at 7:35
2
\$\begingroup\$

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

\$\endgroup\$
  • \$\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 Dec 18 '17 at 13:57
  • \$\begingroup\$ I add two ressources \$\endgroup\$ – Roman Dec 18 '17 at 14:11
1
\$\begingroup\$

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

\$\endgroup\$
1
\$\begingroup\$

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>

\$\endgroup\$
1
\$\begingroup\$

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 the fr "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>

\$\endgroup\$
  • \$\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 Dec 18 '17 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. Dec 18 '17 at 21:25
  • \$\begingroup\$ I have upvoted but due to my low reputation it doesn't really do anything. \$\endgroup\$ – Justin Gagnon Dec 19 '17 at 2:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.