5
\$\begingroup\$

I'm currently learning Vue JS and I made a simple app that pulls information from API and displays facts about a country given a 2-letter country code. I'm looking for feedback on how to improve the code. I'd appreciate any feedback. Many thanks.

This is my app.vue

import axios from "axios"; export default { name: "App", data() { return { userinput: "", country: { name: "", capital: "", currency: "", phone: "", emoji: "", languages: [], continent: "", }, properties: { demonym: "", area: "", timezones: [], population: "" }, }; }, computed: { className: function () { if (this.country.continent) { if (this.country.continent.name == "Europe") return "europe"; else if (this.country.continent.name == "Asia") return "asia"; else if (this.country.continent.name == "Antarctica") return "antarctica"; else if (this.country.continent.name == "Oceania") return "oceania"; else if (this.country.continent.name == "Africa") return "africa"; } return ""; }, }, methods: { onEnter: function () { if (this.userinput !== "") { let body = { query: ` query($code: ID!) { country(code: $code) { name capital currency phone emoji languages {name} continent {name} }}`, variables: { code: this.userinput }, }; let options = { headers: { "Content-Type": "application/json" } }; axios .all([ axios.post( "https://countries.trevorblades.com/graphql", body, options ), axios.get("https://restcountries.com/v2/alpha/" + this.userinput), ]) .then( axios.spread((data1, data2) => { /*console.log(res.data);*/ let countryName = data1.data.data.country.name; let capital = data1.data.data.country.capital; let currency = data1.data.data.country.currency; let phone = data1.data.data.country.phone; let emoji = data1.data.data.country.emoji; let languages = data1.data.data.country.languages; let continent = data1.data.data.country.continent; this.country.name = countryName == null ? "" : countryName; this.country.capital = capital == null ? "" : capital; this.country.currency = currency == null ? "" : currency; this.country.phone = phone == null ? "" : phone; this.country.emoji = emoji == null ? "" : emoji; this.country.languages = languages.length == 0 ? "" : languages; this.country.continent = continent == null ? "" : continent; let demonym = data2.data.demonym; let area = data2.data.area; let timezones = data2.data.timezones; let population = data2.data.population; this.properties.demonym = demonym == null ? "" : demonym; this.properties.area = area == null ? "" : area; this.properties.timezones = timezones.length == 0 ? "" : timezones; this.properties.population = population == null ? "" : population; console.log(this.country.name); console.log(this.country.capital); console.log(this.country.currency); console.log(this.country.phone); console.log(this.country.emoji); console.log(this.country.languages); console.log(this.country.continent); console.log(this.properties.demonym); console.log(this.properties.area); console.log(this.properties.timezones); console.log(this.properties.population); }) ) .catch((error) => { console.log(error.response.data.error); }); } }, nameWithComma(index, name, languageslist) { if (index !== languageslist.length - 1) { return name.concat(",").trim(); } else { return name; } }, convertToLineBreak(txt) { if (!txt) { return txt.replace(txt, "<br>"); } return txt; }, }, }; </script> <template> <div id="app" :class="className"> <main> <div class="search-box"> <input type="text" class="search-bar" placeholder="Enter a two-letter country code..." v-model="userinput" @keyup.enter="onEnter" /> </div> <div v-if="country.name != ''"> <div class="countryname-box"> <div class="name">{{ country.name }}</div> <div class="continent"> {{ country.continent.name }} </div> </div> <div class="emoji-box"> <div class="emoji"> {{ country.emoji }} </div> </div> <div class="countryinfo-box"> <div class="left-container"> <div class="second-half"> <p class="bold">Currency:</p> <p class="bold">Timezone:</p> <p class="bold">Language(s):</p> <p class="bold">Capital:</p> <p class="bold">Area:</p> <p class="bold">Demonym:</p> <p class="bold">Population:</p> <p class="bold">Call Code:</p> </div> </div> <div class="right-container" style="white-space: pre"> <p>{{ convertToLineBreak(country.currency) }}</p> <div class="textinline" v-for="(item, index) in properties.timezones" :key="index" > {{ nameWithComma(index, item, properties.timezones) }} </div> <br /> <div class="textinline" v-for="(item, index) in country.languages" :key="index" > {{ nameWithComma(index, item.name, country.languages) }} </div> <p>{{ country.capital }}</p> <p>{{ properties.area }}</p> <p>{{ properties.demonym }}</p> <p>{{ properties.population }}</p> <p>{{ country.phone }}</p> </div> </div> </div> </main> </div> </template> ``` 
\$\endgroup\$
1
  • \$\begingroup\$ is there css or a styling framework? \$\endgroup\$ Commented Mar 15, 2023 at 11:00

2 Answers 2

5
\$\begingroup\$

Regarding Vue this code looks quite good. I do however have some issues with the general JavaScript and HTML.


I'm not a big fan of using empty strings for missing data. null or undefined would be a better. For one it simplifies reading the data from the API (more on that later).


The calculation of the class name could be much simpler. At the very least use a local variable instead of repeating this.country.continent.name. But the chain of ifs is unnecessary. In the simplest case, just convert the continent name to lower case, (optionally checking it against a list of allowed names). Also the use of newer JavaScript features such as optional chaining (?.) can simplify it even more:

computed: { className() { // Abbreviated syntax for functions in object literals return this.country.continent?.name?.toLowerCase(); } } 

The assignment of the data to your data objects can also be simpified by using modern technics such as destructuring assignments (and dropping the empty strings for missing data). Also use const instead of let for variables that don't change.

axios.spread((trevorbladesData, restcountriesData) => { // Better parameter names const {name, capital, currency, phone, emoji, languages, continent} = trevorbladesData.data.data.country; this country = {name, capital, currency, phone, emoji, languages, continent}; // similar for `properties` } 

There are better ways to add commas between items than nameWithComma.

For example, put the comma into the template. BTW, instead of a div with textinline just use a natural inline element such as span, or if you don't really need the elements at all, just use template. Additionally there should be a surrounding element:

<div> <template v-for="(item, index) in properties.timezones" :key="index" > <template v-if="index !== 0">, </template>{{ item }} </template> </div> 

Another way whould be be to use computed to generate the comma separated list. This is probably the easiest:

computed: { // ... more computed functions timezones() { return properties.timezones.join(", "); } } 

and just use

<div>{{ timezones }}</div> 

in the template.


convertToLineBreak is a very strange way to replace an empty string with a line break. Why not just:

convertToLineBreak(txt) { return txt || "<br>"; } 

Aside from that, the way you are using it will just literally display <br>.

But you shouldn't do this at all anyway because:


The way you are displaying the data in the HTML is terribly broken. It is not accessable (screen readers will first read all the captions and the the data), it requires extra work to makr sure there is always a line (using pre and <br>s) and will break if the data unexpectedly is longer than one line.

In HTML it is important to use the proper semantic markup. In your case you should be using a description list (CSS can be used to have the items appear in a single line) or simply a table:

<table> <tr> <th>Currency:</th> <td>{{ country.currency }}</td> </tr> <tr> <th>Timezone:</th> <td>{{ timezones }}</td> </tr> <!-- etc. --> </table> 
\$\endgroup\$
4
  • 1
    \$\begingroup\$ The OP's code didn't appear to handle cases for the other continents (i.e. "South America" and "North America") but those are possible values so simply returning the lower case of those values would be different than what the OPs code returns. \$\endgroup\$ Commented Mar 15, 2023 at 15:39
  • \$\begingroup\$ tables aren't the most friendly for screen readers either. \$\endgroup\$ Commented Mar 15, 2023 at 17:29
  • \$\begingroup\$ @depperm But they are much better than two unrelated elements. At the very least the entries will be read in the correct order. And there are additional ARIA attributes that can be added to enhance the accessibility. \$\endgroup\$ Commented Mar 15, 2023 at 18:58
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ I did mention that the name could be validated against a list. Or the function could extended to for example replace spaces with hyphens. \$\endgroup\$ Commented Mar 15, 2023 at 19:02
4
\$\begingroup\$

Simplify

Computed className is just lowercase continent name

className: function () { if (this.country.continent) { return this.country.continent.name.toLowerCase() } return '' } 

As you don't resuse any of the variables in axios spread (let countryName = data1.data.data.country.name;) and null displays empty, these lines can be reduced to:

this.country.name = data1.data.data.country.name; this.country.capital = data1.data.data.country.capital; this.country.currency = data1.data.data.country.currency; this.country.phone = data1.data.data.country.phone; this.country.emoji = data1.data.data.country.emoji; this.country.languages = data1.data.data.country.languages.map(l=>l.name); this.country.continent = data1.data.data.country.continent; this.properties.demonym = data2.data.demonym; this.properties.area = data2.data.area; this.properties.timezones = data2.data.timezones; this.properties.population = data2.data.population; 

languages is a list of objects, so you can map it now

query doesn't actually need to be multiline (though this may reduce readability)

query: 'query($code: ID!) {country(code: $code) { name capital currency phone emoji languages {name} continent {name}}}' 

I believe the currency display doesn't need to replace with line break, if it isn't specified the p tag will be empty. The two div with v-for that call nameWithComma should probably be replaced with a p and a join (the css isn't specified, but the method still seems overly complicated)

<p>{{ country.currency }}</p> <p>{{ properties.timezones.join() }}</p> <p>{{ country.languages.join() }}</p> 

Improve Input

Some users may not know the 2 country code of the country they want. Having a select or autocomplete input would probably improve UX (user experience). Minimum effort would be to set maxlength="2" of input

Improve Error Output

If a user currently enters an invalid code nothing in the display specifies something went wrong. Using a select would remove half of these (invalid input), but if the api is down, letting the user know something went wrong would be useful. Add prop error

<div v-if="error">{{ error }}</div> <div v-else> ... axios.spread((data1, data2) => { /*console.log(res.data);*/ this.error = '' .... .catch((error) => { console.log(error.response.data.error, error); this.error = error.message }); 

Full code:

<template> <div id="app" :class="className"> <main> <div class="search-box"> <input type="text" class="search-bar" maxlength="2" placeholder="Enter a two-letter country code..." v-model="userinput" @keyup.enter="onEnter" /> </div> <div v-if="error">{{ error }}</div> <div v-else> <div class="countryname-box"> <div class="name">{{ country.name }}</div> <div class="continent"> {{ country.continent.name }} </div> </div> <div class="emoji-box"> <div class="emoji"> {{ country.emoji }} </div> </div> <div class="countryinfo-box"> <div class="left-container"> <div class="second-half"> <p class="bold">Currency:</p> <p class="bold">Timezone:</p> <p class="bold">Language(s):</p> <p class="bold">Capital:</p> <p class="bold">Area:</p> <p class="bold">Demonym:</p> <p class="bold">Population:</p> <p class="bold">Call Code:</p> </div> </div> <div class="right-container" style="white-space: pre"> <p>{{ country.currency }}</p> <p>{{ properties.timezones.join() }}</p> <p>{{ country.languages.join() }}</p> <p>{{ country.capital }}</p> <p>{{ properties.area }}</p> <p>{{ properties.demonym }}</p> <p>{{ properties.population }}</p> <p>{{ country.phone }}</p> </div> </div> </div> </main> </div> </template> <script> import axios from "axios"; export default { name: "App", data() { return { userinput: "", country: { name: "", capital: "", currency: "", phone: "", emoji: "", languages: [], continent: "", }, properties: { demonym: "", area: "", timezones: [], population: "" }, error: '' }; }, computed: { className: function () { if (this.country.continent) { return this.country.continent.name.toLowerCase(); } return '' }, }, methods: { onEnter: function () { if (this.userinput !== "") { let body = { query: 'query($code: ID!) {country(code: $code) { name capital currency phone emoji languages {name} continent {name}}}', variables: { code: this.userinput }, }; let options = { headers: { "Content-Type": "application/json" } }; axios .all([ axios.post( "https://countries.trevorblades.com/graphql", body, options ), axios.get("https://restcountries.com/v2/alpha/" + this.userinput), ]) .then( axios.spread((data1, data2) => { /*console.log(res.data);*/ this.error = '' this.country.name = data1.data.data.country.name; this.country.capital = data1.data.data.country.capital; this.country.currency = data1.data.data.country.currency; this.country.phone = data1.data.data.country.phone; this.country.emoji = data1.data.data.country.emoji; this.country.languages = data1.data.data.country.languages.map(l=>l.name); this.country.continent = data1.data.data.country.continent; this.properties.demonym = data2.data.demonym; this.properties.area = data2.data.area; this.properties.timezones = data2.data.timezones; this.properties.population = data2.data.population; }) ) .catch((error) => { console.log(error.response.data.error); this.error = error.message }); } } }, }; </script> <style> .left-container { float: left; } .right-container { float: right; } .bold { font-weight: 600; } </style> 
\$\endgroup\$
2
  • \$\begingroup\$ The OP's code didn't appear to handle cases for the other continents (i.e. "South America" and "North America") but those are possible values so simply returning the lower case of those values would be different than what the OPs code returns. \$\endgroup\$ Commented Mar 15, 2023 at 15:40
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ yes/no, its a class name, if OP applies a class without a style its basically the same as not having a class at all. \$\endgroup\$ Commented Mar 15, 2023 at 17:28

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.