Search code examples
javascriptvue.jssortingvuejs2vuex

Sort Computed Object and Array by Two Fields


I am pulling information from an API that returns data in the following format:

[
 {
  "id": 173,
  "date": "2020-12-10T16:05:30",
  "date_gmt": "2020-12-10T16:05:30",
  "guid": {},
  "modified": "2020-12-10T16:05:31",
  "modified_gmt": "2020-12-10T16:05:31",
  "slug": "test",
  "status": "publish",
  "type": "place",
  "link": "http://localhost:81/test/",
  "title": {},
  "content": {},
  "featured_media": 0,
  "template": "",
  "acf": {
    "address": {
    "address": "123 Test Address",
    "street_number": "123",
    "street_name": "Test Address",
    "city": "Philipsburg",
    "state": "Sint Maarten",
    "country": "Sint Maarten",
    "country_short": "SX"
  },
  "header": {}
  },
  "_links": {}
 },
 etc
]

I store that in Vuex, and organize the information via the following:

computed: {
    resorts() {
      const resorts = {};
      if (this.$store.state.loading === false) {
        this.$store.state.posts.forEach((post) => {
          const c = post.acf.address.country;
          const s = post.acf.address.state;
          //const t = post.title;
          resorts[c] = resorts[c] || {};
          resorts[c][s] = resorts[c][s] || [];
          resorts[c][s].push(post);
        });
      }
      return resorts;
    },
}

I'm displaying the information in a v-for loop like this (Pug):

section.united-states(v-for="(country, index) in resorts" v-if="index==='United States'")
  h1(v-html="index")
  section.state(v-for="(state, subIndex) in country" :key="subIndex" :class="subIndex.toLowerCase()")
    h5(v-html="subIndex")
    ul
      li(v-for="post, resort) in state")
        listing(:id="post.id" :slug="post.slug" :image="post.acf.header" :title="post.title.rendered" :city="post.acf.address.city" :street="post.acf.address.street_name_short")

This displays the information correctly. However, I need it organized alphabetically by Country, then State, then City names. I've tried to sort it and attempted lodash.orderBy, but could not get the list organized. From the Vue inspector tab in Chrome, the computed countries and states (not cities) appear to be alphabetical. Any suggestions?


Solution

  • One solution is to sort the posts before grouping them by address.

    Using Array.prototype.sort() and String.prototype.localeCompare(), create a utility (named sortPosts()) to use in the computed prop that will sort the posts by the country, state, city, then street_name fields:

    const sortPosts = posts =>
      posts.slice().sort((a,b) => {
        const countryA = a.acf.address.country
        const countryB = b.acf.address.country
        const stateA = a.acf.address.state
        const stateB = b.acf.address.state
        const cityA = a.acf.address.city || '' // can be undefined in Google Maps API
        const cityB = b.acf.address.city || '' // can be undefined in Google Maps API
        const streetA = a.acf.address.street_name
        const streetB = b.acf.address.street_name
        return countryA.localeCompare(countryB) || stateA.localeCompare(stateB) || cityA.localeCompare(cityB) || streetA.localeCompare(streetB)
      })
    

    Now, we'll group these posts using the same logic you already have, but we have to change the data type of the local resorts variable from Object to Map because Object iteration does not always follow the insertion order, which would break the sorting from sortPosts():

    export default {
      computed: {
        resorts() {
          // BEFORE:
          // const resorts = {};
    
          const resorts = new Map();
    
          if (this.$store.state.loading === false) {
            sortPosts(this.$store.state.posts).forEach((post) => {
              const c = post.acf.address.country;
              const s = post.acf.address.state;
    
              // BEFORE:
              // resorts[c] = resorts[c] || {};
              // resorts[c][s] = resorts[c][s] || [];
              // resorts[c][s].push(post);
    
              if (!resorts.has(c)) {
                resorts.set(c, new Map());
              }
              const stateMap = resorts.get(c);
              if (!stateMap.has(s)) {
                stateMap.set(s, []);
              }
              stateMap.get(s).push(post);
            });
          }
          return resorts
        },
      }
    }
    

    As of v2.6.12, v-for does not support Maps, so use Array.from() to make it iterable in v-for:

    <section v-for="[country, countryData] in Array.from(resorts)" :key="country">
      <h1 v-html="country" />
      <section class="state" v-for="[state, posts] in Array.from(countryData)" :key="state" :class="state.toLowerCase()">
        <h5 v-html="state" />
        <ul>
          <li v-for="(post, resort) in posts" :key="post.id">
            ...
          </li>
        </ul>
      </section>
    </section>
    

    demo