tmswr
tmswr

Reputation: 97

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?

Upvotes: 1

Views: 99

Answers (1)

tony19
tony19

Reputation: 138226

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

Upvotes: 1

Related Questions