World countries search bar with Vue.js (using restcountries API)

262 views Asked by At

The API country data get loaded (see console.log() entry and the added picture of my working desk) but no country info is shown when dragging the mouse cursor into the search bar field (should show the whole country list in a dropdown) or typing a country name into the search bar (filtering out the chosen country name)!What am I missing here in this context?

I just wanted to show the selected country data (capital, currency, language, timezone,...) from the API in the right box and its country flag as background image!

// Format a number with suffix letter
function formatNumber(labelValue) {
  return Math.abs(Number(labelValue)) >= 1.0e+9
    ? Math.abs(Number(labelValue)) / 1.0e+9 + "B"
  : Math.abs(Number(labelValue)) >= 1.0e+6
    ? Math.abs(Number(labelValue)) / 1.0e+6 + "M"
  : Math.abs(Number(labelValue)) >= 1.0e+3
    ? Math.abs(Number(labelValue)) / 1.0e+3 + "K"
  : Math.abs(Number(labelValue));
}

const settings = {
    async: true,
    crossDomain: true,
    url: `https://restcountries.com/v3.1/all`,
    method: 'GET',
    headers: {
        'X-RapidAPI-Key': 'b56451a2dcmsh7ba7285037af267p1435dajsn71561b599c58',
        'X-RapidAPI-Host': 'rest-countries10.p.rapidapi.com'
    }
};

$.ajax(settings).done(function (response) {
    console.log(response);
});

var vm = Vue.createApp({
//var vm = new Vue({
  el: '#main',

  data: {
    search: '',
    selectedName: '',
    selectedCode: '',
    selectedCapital: '',
    selectedLanguage: '',
    selectedCurrency: '',
    selectedLatlng: '',
    selectedLatlngUrl: '',
    selectedPopulation: '',
    selectedArea: '',
    selectedSubregion: '',
    selectedGini: '',
    selectedTimezone: '',
    selectedFlagSrc: '',
  },

  computed: {
    countries () {
      var self = this;
      var countries;

      // Get JSON from API
      $.ajax({
        async: false,
          //crossDomain: true,
        //method:'GET',
        //url: 'https://cors-anywhere.herokuapp.com/https://restcountries.com/v3.1/all',
        url: `https://restcountries.com/v3.1/all`,
        success: function(data){
          countries = data;
        } 
      });
        
      // Filter search by country name in all languages
      countries = countries.filter(function(value, index, array) {
        return value['name'].toLowerCase().includes(self.search.toLowerCase())
        || Object.values(value['translations']).join(' ').toLowerCase().includes(self.search.toLowerCase());
      });
      return countries; 
    },
  },

  // Select first country in list on load
  beforeMount() {
    var self = this;
    var found = false;
    this.countries.forEach(function(element) {
      if(element['alpha2Code'] === 'US') {
        self.selectCountry(element);
        found = true;
      }
    });      

    if(!found)
        this.selectCountry(this.countries[0]);

  },        

  methods: {
    // Returns country name
    getName (country) {
      return (country['name']);
    },

    // Returns country flag URL
    getFlagSrc (country) {
      return (country['flag'] || 'N/A');
    },

    // Set country data
    selectCountry (country) {
      var self = this;

      $('section').animate({
        opacity: 0
      }, 150, function() {

        self.selectedName = (country['name'] || 'N/A');
        self.selectedFlagSrc = self.getFlagSrc(country);
        self.selectedCode = (country['alpha2Code'] || 'N/A') + ' / ' + (country['alpha3Code'] || 'N/A');
        self.selectedCapital = (country['capital'] || 'N/A');

        var arrayLanguage = [];
        country['languages'].forEach(function(element) {
          arrayLanguage.push(element['name']);
        });
        self.selectedLanguage = (country['languages'].length > 0) ? arrayLanguage.join(', ') : 'N/A';

        var arrayCurrency = [];
        country['currencies'].forEach(function(element) {
          arrayCurrency.push(element['name'] + ' ' + element['symbol']);
        });
        self.selectedCurrency = (country['currencies'].length > 0) ? arrayCurrency.join(', ') : 'N/A';

        self.selectedLatlng = (country['Latlng'].length > 0) ? ('Lat: ' + country['Latlng'][0] + ', Lng: ' + country['Latlng'][1]) : 'N/A';
        self.selectedLatlngUrl = (country['Latlng'].length > 0) ? ('https://www.google.com/maps/?q=' + country['Latlng'][0] + ',' + country['Latlng'][1]) : '';
        self.selectedPopulation = country['Population'] ? formatNumber(country['Population']) : 'N/A';
        self.selectedArea = country['Area'] ? (formatNumber(country['Area']) + ' kmĀ²') : 'N/A';
        self.selectedSubregion = (country['Subregion'] || 'N/A');
        self.selectedGini = country['Gini'] ? (country['Gini'] + '%') : 'N/A';
        self.selectedTimezone = (country['Timezones'].length > 0) ? country['Timezones'].join(', ') : 'N/A';

        $('section').animate({
          opacity: 1
        });
      });
    },
  }

});

output image

1

There are 1 answers

1
Andres Abadia On

Generally is not a good idea to make async calls in a computed property. Every time the user is typing and updating the search variable the computed property is being call ending will tons of request per user. You should rather make that call once and populate a variable with the results on a life cicle hook like created, mounted, etc:

data() {
    return {
      countries: [],
    };
  },
  mounted() {
    (async () => {
      const response = await fetch("https://restcountries.com/v3.1/all");
      this.countries = await response.json();
    })();
  },

Once you have your list of countries you can now filter those on your computed property:

data() {
    return {
      countries: [],
      search: "",
    };
  },
  mounted() {
    (async () => {
      const response = await fetch("https://restcountries.com/v3.1/all");
      this.countries = await response.json();
    })();
  },
  computed: {
    filteredCountries() {
      if (this.countries.length === 0) return []; // just making sure that countries is already populated
      return this.countries.filter((country) => {
        return country.name.common
          .toLowerCase()
          .includes(this.search.toLowerCase());
      });
    },
  },

Now is up to you to create a logic on how the user select a filtered country. It could be a click on the list and the selected country is displayed. For the sake of make the example complete I created another computer property that return the first country of the filtered list and a click listener in the list that populates the search field.

 data() {
    return {
      countries: [],
      search: "",
    };
  },
  mounted() {
    (async () => {
      const response = await fetch("https://restcountries.com/v3.1/all");
      this.countries = await response.json();
    })();
  },
  computed: {
    filteredCountries() {
      if (this.countries.length === 0) return [];
      return this.countries.filter((country) => {
        return country.name.common
          .toLowerCase()
          .includes(this.search.toLowerCase());
      });
    },
    selectedCountry() {
      return this.filteredCountries[0];
    },
  },
  methods: {
    selectCountry(index) {
      this.search = this.filteredCountries[index].name.common;
    },
  },

Check out a working example. https://jsfiddle.net/andresabadia/n0acsj5x/5/

const {
  createApp
} = Vue

createApp({
  data() {
    return {
      countries: [],
      search: "",
    };
  },
  mounted() {
    (async() => {
      const response = await fetch("https://restcountries.com/v3.1/all");
      this.countries = await response.json();
    })();
  },
  computed: {
    filteredCountries() {
      if (this.countries.length === 0) return [];
      return this.countries.filter((country) => {
        return country.name.common
          .toLowerCase()
          .includes(this.search.toLowerCase());
      });
    },
    selectedCountry() {
      return this.filteredCountries[0];
    },
  },
  methods: {
    selectCountry(index) {
      this.search = this.filteredCountries[index].name.common;
    },
  },
}).mount('#app')
li {
  cursor: pointer
}
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">
  <label for="country">country search</label>
  <input id="country" type="text" v-model="search" />
  <ul>
    <li v-for="(country, index) in filteredCountries" @click="selectCountry(index)">
      {{ country.name.common }}
    </li>
  </ul>
  <div v-if="selectedCountry">
    <h2>{{ selectedCountry.name.common }}</h2>
    <img :src="selectedCountry.flags.png" alt="" />
    <p v-for="capital in selectedCountry.capital">{{ capital }}</p>
  </div>
</div>