<div class="flex items-center justify-center w-full mb-6">
  <div id="app">
    
    <!-- Breed Options -->
    <div class="flex justify-center">
      <button
        v-for="(breed, i) in breeds"
        :key="i"
        class="hover:bg-blue-500 text-sm py-1 px-2 border border-blue-500 rounded m-2"
        :class="{
          'text-blue-500 hover:text-white': breed.key !== breedKey,
          'bg-blue-500 text-white': breed.key === breedKey
        }"
        @click="currentBreed = i"
      >
        {{ breed.name }}
      </button>
    </div>
    
    <div class="rounded shadow-md bg-gray-100 mt-4">
    
      <!-- Async Component -->
      <async :url="`https://dog.ceo/api/breed/${breedKey}/images`">

        <!-- Scoped Slot of Async Component -->
        <template v-slot:default="{ pending, error, data }">

          <!-- Request Pending -->
          <div v-if="pending" class="text-center">
            <img src="https://files.codethink.de/public/Preloader_2.gif" alt="loading" class="mx-auto">
            <span class="text-gray-400 font-medium">Loading ...</span>
          </div>

          <!-- Request Error -->
          <div v-else-if="error" class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
            {{ error }}
          </div>

          <!-- Request Success -->
          <ul v-else class="flex flex-wrap justify-between">
            <li v-for="(image, i) in data.message.slice(0,24)" :key="i" class="m-2">
              <img :src="image" class="rounded h-24" />
            </li>
          </ul>
        
        </template>
      </async>
    </div>
    
  </div>
</div>

<div class="bg-gray-400 text-gray-600 px-4 py-3 rounded relative text-sm text-center"><a href="https://twitter.com/_lhermann" class="text-blue-600 hover:underline">Follow me on Twitter</a></div>

/**
 * Async Component
 * 
 * This is a renderless component.
 * It is an abstraction for GET requests.
 * Pass a mandatory "url" and an
 * optional "params" object.
 */
const Async = Vue.component('async', {
  props: {
    url: { type: String, default: "", required: true },
    params: { type: Object, default: () => ({}) }
  },
  data() {
    return {
      pending: true,
      error: false,
      data: null
    };
  },
  watch: {
    url() {
      this.requestData();
    },
    params: {
      handler() {
        this.requestData();
      },
      deep: true
    }
  },
  mounted() {
    this.requestData();
  },
  methods: {
    async requestData() {
      this.pending = true;
      try {
        const { data } = await axios.get(this.url, { params: this.params });
        this.data =  data;
        this.error = false;
      } catch (e) {
        this.data = null;
        this.error = e;
      }
      this.pending = false;
    }
  },
  render() {
    return this.$scopedSlots.default({
      pending: this.pending,
      error: this.error,
      data: this.data
    });
  }
});


/*
 * Instantiate Vue App
 */
new Vue({
  el: '#app',
  components: { Async },
  data() {
    return {
      currentBreed: 0,
      breeds: [
        { name: "Golden Retriever", key: "retriever/golden" },
        { name: "German Shepherd", key: "germanshepherd" },
        { name: "Husky", key: "husky" },
        { name: "Pug", key: "pug" },
        { name: "(Error)", key: "error" },
      ]
    }
  },
  computed: {
    breedKey() {
      return this.breeds[this.currentBreed].key;
    }
  }
})

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/1.0.6/tailwind.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js