#app(v-bind:data-state="gallery")
  form.ui-form(v-on:submit.prevent="transition({ type: 'SEARCH', query: this.query })")
    input.ui-input(
      placeholder="Search Flickr for photos..."
      type="search"
      v-model="query"
      v-bind:disabled="gallery === 'loading'")
    .ui-buttons
      button.ui-button(
        v-bind:disabled="gallery === 'loading'") {{searchText}}
      button.ui-button(
        v-if="gallery === 'loading'"
        type="button"
        v-on:click="transition({ type: 'CANCEL_SEARCH' })") Cancel
        
  section.ui-items
    span.ui-error(v-cloak v-if="gallery === 'error'") Uh oh, search failed.
    img.ui-item(v-else
      v-for="item in items"
      v-bind:key="item"
      v-bind:src="item"
      v-on:click="transition({ type: 'SELECT_PHOTO', item })")
      
  section.ui-photo-detail(
    v-if="gallery === 'photo'"
    v-on:click="transition({ type: 'EXIT_PHOTO' })")
    img.ui-photo(v-bind:src="photo")
View Compiled
textarea
  padding: 1rem
h1
  text-align: center
  font-weight: 100

#app
  &:after
    content: 'current state: ' attr(data-state)
    position: fixed
    bottom: .5rem
    color: white
    background-color: rgba(black, 0.4)
    padding: .5rem 1rem
    border-radius: 1rem
    left: 50%
    transform: translateX(-50%)
    text-shadow: 0 0 .1rem black
    pointer-events: none

.ui-form
  display: flex
  margin: 0 auto
  width: auto
  position: sticky
  justify-content: center
  padding: 1rem
  button
    padding: 0.5rem
    margin-left: 0.5rem
    
.ui-items
  display: grid
  grid-gap: 1rem
  padding: 1rem
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr))
  grid-template-rows: minmax(160px, max-content)
  
.ui-item
  object-fit: cover
  width: 100%
  height: 100%
  
.ui-photo-detail
  position: absolute
  left: 0
  top: 0
  right: 0
  bottom: 0
  display: flex
  align-items: center
  justify-content: center
  background-color: alpha(#FFF, 80%)
  
.ui-photo
  max-height: 100vh
  max-width: 100vw
Vue.config.devtools = true

const transitions = {
  start: {
    SEARCH: 'loading'
  },
  loading: {
    SEARCH_SUCCESS: 'gallery',
    SEARCH_FAILURE: 'error',
    CANCEL_SEARCH: 'gallery'
  },
  error: {
    SEARCH: 'loading'
  },
  gallery: {
    SEARCH: 'loading',
    SELECT_PHOTO: 'photo'
  },
  photo: {
    EXIT_PHOTO: 'gallery'
  }
}

const app = new Vue({
	el: '#app',
	data() {
    return {
      gallery: 'start', // finite state
      query: '',
      items: [],
      photo: ''
    }
  },
	computed: {
		searchText() {
      return {
        loading: 'Searching...',
        error: 'Try search again',
        start: 'Search'
      }[this.gallery] || 'Search'
    }
	},
  methods: {
    command(nextState, action) { // Logic
      switch (nextState) {
        case 'loading':
          this.search(action.query)
          break
        case 'gallery':
          if (action.items) return { items: action.items }
          break
        case 'photo':
          if (action.item) return { photo: action.item }
          break
      }
    },
    transition(action) {
      const next = transitions[this.gallery][action.type]

      next && Object.assign(app.$data, {
        gallery: next,
        ...this.command(next, action)
      })
    },
    search(query) {
      const encodedQuery = encodeURIComponent(query)
      const url = `https://dog.ceo/api/breeds/image/random/6`
      
      fetch(url)
        .then(response => response.json())
        .then(data => {
          this.transition({ type: 'SEARCH_SUCCESS', items: data.message })
        })
        .catch(error => {
          this.transition({ type: 'SEARCH_FAILURE' })
        })
    }
  }
})
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/vue
  2. https://unpkg.com/fetch-jsonp@1.1.3/build/fetch-jsonp.js