Programación usando VueJS (Componentes en Vue). Parte 4

El sistema de componentes

El Sistema de componentes de Vue es una parte importante ya que nos permite escribir nuestro propios elementos html.

Desde Vue tenemos 2 maneras de registrar los componentes:

  • Global: pueden usarse en toda la aplicación.
  • Local: indicamos donde y que componente queremos usar.

Para crear un componente existen varias formas, pero en nuestro ejemplo vamos a crear todo el componente en un único fichero.

Para comenzar, vamos a usar el fichero App.vue del ejemplo creado en la tercera parte de este curso.

<template>
  <div id="app" v-cloak>
    <header>
      <div v-text="header.title" class="title"></div>
      <div v-text="header.subtitle" class="subtitle"></div>
    </header>
    <section class="container">
      <div class="box-search">
        <input type="search" v-model="search">
        <a v-show="checkSearch" @click="searchFilm()"><font-awesome-icon icon="search" /></a>
        <div class="pagination" v-show="showPagination">
          <span class="reg-prev" @click="previous()">
            <font-awesome-icon icon="angle-left" />
          </span>
          <span class="total-pages" v-text="printPages"></span>
          <span class="reg-next" @click="next()">
            <font-awesome-icon icon="angle-right" />
          </span>
        </div>
      </div>
      <div class="box-result">
        <ul v-show="showFilm">
          <li v-for="(film, index) in dataFilm" :key="index">
            <div :class="film.Poster !== 'N/A' ? 'image-film' : 'image-film border'"><img :src="film.Poster !== 'N/A' ? film.Poster : noImage" alt=""></div>
            <div class="image-data">
              <table>
                <tr>
                  <th>ID<font-awesome-icon icon="info-circle" @click="searchDetailFilm(film.imdbID, 'id')" class="icon-right"/></th>
                  <td v-text="film.imdbID"></td>
                </tr>
                <tr><th>Title</th><td v-text="film.Title"></td></tr>
                <tr><th>Year</th><td v-text="film.Year"></td></tr>
                <tr><th>Type</th><td v-text="film.Type"></td></tr>
                <tr><th>Poster</th><td><a :href="film.Poster" target="_blank" class="btn-download"><font-awesome-icon icon="download" /> Poster</a></td></tr>
              </table>
            </div>
          </li>
        </ul>
        <ul v-show="showDetailFilm">
          <div :class="dataDetailFilm.Poster !== 'N/A' ? 'image-film' : 'image-film border'">
            <img :src="dataDetailFilm.Poster" alt="">
          </div>
          <div class="image-data">
            <table>
              <tr><th>Title</th><td v-text="dataDetailFilm.Title"></td></tr>
              <tr><th>Year</th><td v-text="dataDetailFilm.Year"></td></tr>
              <tr><th>Rated</th><td v-text="dataDetailFilm.Rated"></td></tr>
              <tr><th>Released</th><td v-text="dataDetailFilm.Released"></td></tr>
              <tr><th>Runtime</th><td v-text="dataDetailFilm.Runtime"></td></tr>
              <tr><th>Genre</th><td v-text="dataDetailFilm.Genre"></td></tr>
              <tr><th>Director</th><td v-text="dataDetailFilm.Director"></td></tr>
              <tr><th>Writer</th><td v-text="dataDetailFilm.Writer"></td></tr>
              <tr><th>Actors</th><td v-text="dataDetailFilm.Actors"></td></tr>
              <tr><th>Plot</th><td v-text="dataDetailFilm.Plot"></td></tr>
              <tr><th>Language</th><td v-text="dataDetailFilm.Language"></td></tr>
              <tr><th>Country</th><td v-text="dataDetailFilm.Country"></td></tr>
              <tr><th>Awards</th><td v-text="dataDetailFilm.Awards"></td></tr>
              <tr>
                <th>Ratings</th>
                <td>
                  <tr>
                    <td v-for="(rating, index) in dataDetailFilm.Ratings" :key="index">
                      <b>Source:</b>&nbsp;{{rating.Source}}<br>
                      <b>Value:</b>&nbsp;{{rating.Value}}<br>
                    </td>
                  </tr>
                </td>
              </tr>
              <tr><th>Metascore</th><td v-text="dataDetailFilm.Metascore"></td></tr>
              <tr><th>imdbRating</th><td v-text="dataDetailFilm.imdbRating"></td></tr>
              <tr><th>imdbVotes</th><td v-text="dataDetailFilm.imdbVotes"></td></tr>
              <tr><th>imdbID</th><td v-text="dataDetailFilm.imdbID"></td></tr>
              <tr><th>Type</th><td v-text="dataDetailFilm.Type"></td></tr>
              <tr><th>DVD</th><td v-text="dataDetailFilm.DVD"></td></tr>
              <tr><th>BoxOffice</th><td v-text="dataDetailFilm.BoxOffice"></td></tr>
              <tr><th>Production</th><td v-text="dataDetailFilm.Production"></td></tr>
              <tr><th>Website</th><td><a :href="dataDetailFilm.Website" target="_blank">{{dataDetailFilm.Website}}</a></td></tr>
            </table>
          </div>
        </ul>
      </div>
      <div class="error" v-show="showError">
        {{this.msnError}}
      </div>
      <div class="lds-ripple" v-show="showloading">
        <div></div>
        <div></div>
      </div>
    </section>
  </div>
</template>

<script>
import API from './servicios.js'
export default {
  name: 'app',
  data () {
    return {
      header: {
        title: 'OMDb API',
        subtitle: 'The Open Movie & TV Show Database'
      },
      showButton: false,
      search: '',
      dataFilm: '',
      showError: false,
      msnError: '',
      dataDetailFilm: '',
      showPagination: false,
      pagination: {
        pagActual: 1,
        totalRecords: 0,
        totalPages: 0
      },
      showloading: false,
      noImage: './dist/no-image.png'
    }
  },
  methods: {
    searchFilm () {
      this.showloading = true
      const params = {
        title: this.search,
        page: this.pagination.pagActual
      }
      API.getFilmData(params)
        .then(data => {
          this.showloading = false
          this.dataDetailFilm = ''
          if (data.Response === 'True') {
            this.showError = false
            this.msnError = ''
            this.dataFilm = data.Search
            this.pagination.totalRecords = data.totalResults
            this.pagination.totalPages = (data.totalResults / 10) % 1 === 0 ? parseInt((data.totalResults / 10)) : parseInt((data.totalResults / 10)) + 1
            this.showPagination = true
          } else {
            this.dataFilm = ''
            this.showError = true
            this.msnError = data.Error
            this.showPagination = false
          }
        })
        .catch(err => {
          this.showloading = false
          console.log(`Error App.vue ${err}`)
        })
    },
    searchDetailFilm (value, type) {
      this.showloading = true
      const params = {
        [type]: value
      }
      API.getDetailFilmData(params)
        .then(data => {
          this.showloading = false
          this.dataFilm = ''
          this.showPagination = false
          if (data.Response === 'True') {
            this.showError = false
            this.msnError = ''
            this.dataDetailFilm = data
          } else {
            this.dataDetailFilm = ''
            this.showError = true
            this.msnError = data.Error
          }
        })
        .catch(err => {
          this.showloading = false
          console.log(`Error App.vue ${err}`)
        })
    },
    previous () {
      this.pagination.pagActual = --this.pagination.pagActual < 1 ? 1 : this.pagination.pagActual--
      this.searchFilm()
    },
    next () {
      this.pagination.pagActual = ++this.pagination.pagActual > this.pagination.totalPages ? this.pagination.totalPages : this.pagination.pagActual++
      this.searchFilm()
    }
  },
  computed: {
    checkSearch () {
      return this.search.length === 0 ? false : true
    },
    showFilm () {
      return this.dataFilm === '' ? false : true
    },
    showDetailFilm () {
      return this.dataDetailFilm === '' ? false : true
    },
    printPages () {
      return `${this.pagination.pagActual} de ${this.pagination.totalPages}`
    }
  }
}
</script>

<style lang="scss">
@import url('../node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css');
@media (min-width: 1200px) {
  ul {
    li {
      width: 48%;
    }
  }
}
@media (max-width: 1199px) {
  ul {
    li {
      width: 100%;
    }
  }
}
.lds-ripple {
  display: inline-block;
  position: absolute;
  width: 64px;
  height: 64px;
  top: calc(50% - 32px);
}
.lds-ripple div {
  position: absolute;
  border: 4px solid rgb(20, 59, 235);
  opacity: 1;
  border-radius: 50%;
  animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
  animation-delay: -0.5s;
}
@keyframes lds-ripple {
  0% {
    top: 28px;
    left: 28px;
    width: 0;
    height: 0;
    opacity: 1;
  }
  100% {
    top: -1px;
    left: -1px;
    width: 58px;
    height: 58px;
    opacity: 0;
  }
}
table {
  width: 100%;
  tr {
    td, th {
      border: 1px solid #cccccc;
      padding: 5px;
      margin: 0;
      text-align: left;
    }
  }
}
.pagination {
  float: right;
  width: 100px;
  text-align: center;
  padding: 3px;
  span {
    cursor: pointer;
  }
}
.btn-download {
  background: #0095ff;
  padding: 5px 20px 4px 20px;
  font-size: 11px;
  text-align: center;
  color: #ffffff;
  border-radius: 3px;
}
a {
  cursor: pointer;
  text-decoration: none;
}
.icon-right {
  float: right;
  cursor: pointer;
}
.error {
  background: #ef8c8c;
  padding: 20px;
  color: #ffffff;
}
.border {
  border: 1px solid #cccccc;
  background-image: url('./assets/no-image.png');
  background-repeat: no-repeat;
  background-position: center;
  width: 298px !important;
}
.image-film {
  width: 300px;
  float: left;
  margin: 0 20px 20px 0;
  height: 450px;
}
.image-data {
  border: 1px solid #cccccc;
  float: left;
  width: calc(100% - 332px);
  height: 448px;
  overflow-y: scroll;
}
.box-search {
  border: 1px solid #cccccc;
  padding: 10px;
  text-align: left;
  input[type='search'] {
    width: 60%;
  }
  .box-search button {
    float: right;
  }
}
ul {
   li {
    text-align: left;
    img {
      box-shadow: 0px 0px 30px rgba(0,0,0,1);
      height: 450px;
      width: 300px;
    }
  }
}
[v-cloak] {
  display: none;
}
header {
  box-shadow: 0px 0px 30px rgba(0,0,0,0.15);
  padding: 10px;
}
.container {
  box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.15);
  margin-top: 20px;
  padding: 30px;
  min-height: 500px;
}
.title {
  font-size: 3em;
}
.subtitle {
  font-size: 1em;
}
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  min-width: 850px;
}
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

En ese fichero esta toda la aplicación y como puedes comprobar es un bastante código, ya que la finalidad del mismo era entender como funciona Vue y que con poco conocimiento sobre el framework se pueden hacer bastantes cosas.

Esta forma de trabajar conlleva varios problemas asociados:

  • Mantener y mejorar el código se convierte en una tarea bastante complicada.
  • No se puede reutilizar para otras aplicaciones y/o partes de la aplicación.
  • Cualquier cambio se convierte en un tarea que demanda mucho tiempo.
  • Solucionar cualquier incidencia se convierte en un misión que en ocasiones lleva mas tiempo que un nuevo desarrollo.

Creando nuestros componentes

Para comenzar crearemos una carpeta dentro de src llamada components, y dentro de esa carpeta crearemos otras dos carpetas llamadas local global.

Dentro de la carpeta global crearemos dos componentes, uno llamado filmLoading.vue filmModal.vue.

filmModal.vue (aviso de errores genérico para toda la aplicación)

<template>
  <div :class="typeError" v-show="showModal" v-text="msnModal"></div>
</template>
<script>
  export default {
    props: ['typeError','msnModal','showModal'],
    data () {
      return {
        type: null
      }
    },
    computed: {
      typeModal () {
        return this.type
      }
    }
  }
</script>
<style scoped>
  .error {
    background: #ef8c8c;
    padding: 20px;
    color: #ffffff;
  }
</style>

filmLoading.vue (Icono cargando para toda la aplicación)

<template>
  <div class="lds-ripple" v-show="showLoading">
    <div></div>
    <div></div>
  </div>
</template>
<script>
  export default {
    props: ['showLoading']
  }
</script>
<style scoped>
  .lds-ripple {
    display: inline-block;
    position: absolute;
    width: 64px;
    height: 64px;
    top: calc(50% - 32px);
  }
  .lds-ripple div {
    position: absolute;
    border: 4px solid rgb(20, 59, 235);
    opacity: 1;
    border-radius: 50%;
    animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
  }
  .lds-ripple div:nth-child(2) {
    animation-delay: -0.5s;
  }
  @keyframes lds-ripple {
    0% {
      top: 28px;
      left: 28px;
      width: 0;
      height: 0;
      opacity: 1;
    }
    100% {
      top: -1px;
      left: -1px;
      width: 58px;
      height: 58px;
      opacity: 0;
    }
  }
</style>

La estructura de nuestros componentes como podemos ver, se divide en varias partes y no todas son obligatorias.

<template>
  <div>
  </div>
</template>
<script>
</script>
<style>
</style>

template: Aquí añadiremos todo el html y solo tendrán un único elemento padre.

script: Código Javascript

style: CSS

Con los ficheros anteriores (filmModal.vue filmLoading.vue) hemos creado unos componentes y ahora veremos como registrarlos de forma global.

Dentro del fichero main.js importaremos los componentes y con vue.component registraremos de forma global.

import Vue from 'vue'
import App from './App.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import FilmLoading from './components/global/filmLoading.vue'
import FilmModal from './components/global/filmModal.vue'

/*
 * Importar componente de forma global
 * Vue.component(tag-html, componente)
 */

Vue.component('film-loading', FilmLoading)
Vue.component('film-modal', FilmModal)

library.add(fas)
Vue.component('font-awesome-icon', FontAwesomeIcon)

new Vue({
  el: '#app',
  render: h => h(App)
})

Para finalizar vamos a usar nuestros componente en la aplicación y para ello crearemos una etiqueta en html con el nombre que usamos para registrar el componente.

<film-modal type-error="error" :msn-modal="msnError" :show-modal="showError"></film-modal>
<film-loading :show-loading="showloading"></film-loading>

Con esto ya tendríamos nuestro componente registrado de forma global y funcionando en cualquier parte de la aplicación.

Cuando necesitemos registrar y usar un componente de forma local, el proceso es mas sencillo.

Deberemos importar el fichero donde vayamos a usarlo y registrarlo dentro de nuestro components.

filename: App.vue
......
      <div class="box-result">
        <film-card v-show="showFilm" :data-film="dataFilm" :no-image="noImage"></film-card>
        <film-card-detail></film-card-detail>
      </div>
      <film-modal type-error="error" :msn-modal="msnError" :show-modal="showError"></film-modal>
      <film-loading :show-loading="showloading"></film-loading>
    </section>
  </div>
</template>

<script>
import API from './servicios.js'
/*
 * Importando un componente de forma local.
 * Solo lo usamos donde se importa
 */
import FilmCard from './components/local/filmCard.vue'
import FilmCardDetail from './components/local/filmCardDetail.vue'
export default {
  components: {
    FilmCard,
    FilmCardDetail
  },
  name: 'app',
  data () {
......

A continuación la evolución de nuestra aplicación.

App.vue

<template>
  <div id="app" v-cloak>
    <header>
      <div v-text="header.title" class="title"></div>
      <div v-text="header.subtitle" class="subtitle"></div>
    </header>
    <section class="container">
      <div class="box-search">
        <input type="search" v-model="search">
        <a v-show="checkSearch" @click="searchFilm()"><font-awesome-icon icon="search" /></a>
        <div class="pagination" v-show="showPagination">
          <span class="reg-prev" @click="previous()">
            <font-awesome-icon icon="angle-left" />
          </span>
          <span class="total-pages" v-text="printPages"></span>
          <span class="reg-next" @click="next()">
            <font-awesome-icon icon="angle-right" />
          </span>
        </div>
      </div>
      <div class="box-result">
        <film-card v-show="showFilm" :data-film="dataFilm" :no-image="noImage"></film-card>
        <film-card-detail :data-detail-film="dataDetailFilm" show-detail-film="showDetailFilm"></film-card-detail>
      </div>
      <film-modal type-error="error" :msn-modal="msnError" :show-modal="showError"></film-modal>
      <film-loading :show-loading="showloading"></film-loading>
    </section>
  </div>
</template>

<script>
import API from './servicios.js'
/*
 * Importando un componente de forma local.
 * Solo lo usamos donde se importa
 */
import FilmCard from './components/local/filmCard.vue'
import FilmCardDetail from './components/local/filmCardDetail.vue'
export default {
  components: {
    FilmCard,
    FilmCardDetail
  },
  name: 'app',
  data () {
    return {
      header: {
        title: 'OMDb API',
        subtitle: 'The Open Movie & TV Show Database'
      },
      showButton: false,
      search: '',
      dataFilm: '',
      showError: false,
      msnError: '',
      dataDetailFilm: '',
      showPagination: false,
      pagination: {
        pagActual: 1,
        totalRecords: 0,
        totalPages: 0
      },
      showloading: false,
      noImage: './dist/no-image.png'
    }
  },
  methods: {
    searchFilm () {
      this.showloading = true
      const params = {
        title: this.search,
        page: this.pagination.pagActual
      }
      API.getFilmData(params)
        .then(data => {
          this.showloading = false
          this.dataDetailFilm = ''
          if (data.Response === 'True') {
            this.showError = false
            this.msnError = ''
            this.dataFilm = data.Search
            this.pagination.totalRecords = data.totalResults
            this.pagination.totalPages = (data.totalResults / 10) % 1 === 0 ? parseInt((data.totalResults / 10)) : parseInt((data.totalResults / 10)) + 1
            this.showPagination = true
          } else {
            this.dataFilm = ''
            this.showError = true
            this.msnError = data.Error
            this.showPagination = false
          }
        })
        .catch(err => {
          this.showloading = false
          console.log(`Error App.vue ${err}`)
        })
    },
    searchDetailFilm (value, type) {
      this.showloading = true
      const params = {
        [type]: value
      }
      API.getDetailFilmData(params)
        .then(data => {
          debugger
          this.showloading = false
          this.dataFilm = ''
          this.showPagination = false
          if (data.Response === 'True') {
            this.showError = false
            this.msnError = ''
            this.dataDetailFilm = data
          } else {
            this.dataDetailFilm = ''
            this.showError = true
            this.msnError = data.Error
          }
        })
        .catch(err => {
          this.showloading = false
          console.log(`Error App.vue ${err}`)
        })
    },
    previous () {
      this.pagination.pagActual = --this.pagination.pagActual < 1 ? 1 : this.pagination.pagActual--
      this.searchFilm()
    },
    next () {
      this.pagination.pagActual = ++this.pagination.pagActual > this.pagination.totalPages ? this.pagination.totalPages : this.pagination.pagActual++
      this.searchFilm()
    }
  },
  computed: {
    checkSearch () {
      return this.search.length === 0 ? false : true
    },
    showFilm () {
      return this.dataFilm === '' ? false : true
    },
    showDetailFilm () {
      return this.dataDetailFilm === '' ? false : true
    },
    printPages () {
      return `${this.pagination.pagActual} de ${this.pagination.totalPages}`
    }
  }
}
</script>

<style lang="scss">
  @import url('../node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css');
  @media (min-width: 1200px) {
    ul {
      li {
        width: 48%;
      }
    }
  }
  @media (max-width: 1199px) {
    ul {
      li {
        width: 100%;
      }
    }
  }
  .box-search {
    border: 1px solid #cccccc;
    padding: 10px;
    text-align: left;
    input[type='search'] {
      width: 60%;
    }
    .box-search button {
      float: right;
    }
  }
  [v-cloak] {
    display: none;
  }
  .pagination {
    float: right;
    width: 100px;
    text-align: center;
    padding: 3px;
    span {
      cursor: pointer;
    }
  }
  header {
    box-shadow: 0px 0px 30px rgba(0,0,0,0.15);
    padding: 10px;
  }
  .container {
    box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.15);
    margin-top: 20px;
    padding: 30px;
    min-height: 500px;
  }
  .title {
    font-size: 3em;
  }
  .subtitle {
    font-size: 1em;
  }
  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    min-width: 850px;
  }
  h1, h2 {
    font-weight: normal;
  }
</style>

filmModal.vue

<template>
  <div :class="typeError" v-show="showModal" v-text="msnModal"></div>
</template>
<script>
  export default {
    props: ['typeError','msnModal','showModal'],
    data () {
      return {
        type: null
      }
    },
    computed: {
      typeModal () {
        return this.type
      }
    }
  }
</script>
<style scoped>
  .error {
    background: #ef8c8c;
    padding: 20px;
    color: #ffffff;
  }
</style>

filmLoading.vue

<template>
  <div class="lds-ripple" v-show="showLoading">
    <div></div>
    <div></div>
  </div>
</template>
<script>
  export default {
    props: ['showLoading']
  }
</script>
<style scoped>
  .lds-ripple {
    display: inline-block;
    position: absolute;
    width: 64px;
    height: 64px;
    top: calc(50% - 32px);
  }
  .lds-ripple div {
    position: absolute;
    border: 4px solid rgb(20, 59, 235);
    opacity: 1;
    border-radius: 50%;
    animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
  }
  .lds-ripple div:nth-child(2) {
    animation-delay: -0.5s;
  }
  @keyframes lds-ripple {
    0% {
      top: 28px;
      left: 28px;
      width: 0;
      height: 0;
      opacity: 1;
    }
    100% {
      top: -1px;
      left: -1px;
      width: 58px;
      height: 58px;
      opacity: 0;
    }
  }
</style>

filmCard.vue

<template>
  <ul>
    <li v-for="(film, index) in dataFilm" :key="index">
      <div :class="film.Poster !== 'N/A' ? 'image-film' : 'image-film border'"><img :src="film.Poster !== 'N/A' ? film.Poster : noImage" alt=""></div>
      <div class="image-data">
        <table>
          <tr>
            <th>ID<font-awesome-icon icon="info-circle" @click="searchDetailFilm(film.imdbID, 'id')" class="icon-right"/></th>
            <td v-text="film.imdbID"></td>
          </tr>
          <tr><th>Title</th><td v-text="film.Title"></td></tr>
          <tr><th>Year</th><td v-text="film.Year"></td></tr>
          <tr><th>Type</th><td v-text="film.Type"></td></tr>
          <tr><th>Poster</th><td><a :href="film.Poster" target="_blank" class="btn-download"><font-awesome-icon icon="download" /> Poster</a></td></tr>
        </table>
      </div>
    </li>
  </ul>
</template>
<!-- -->
<script>
  export default {
    props: ['dataFilm','noImage'],
    data () {
      return {
      }
    }
  }
</script>
<!-- -->
<style scoped lang="scss">
  ul {
    li {
      text-align: left;
      img {
        box-shadow: 0px 0px 30px rgba(0,0,0,1);
        height: 450px;
        width: 300px;
      }
    }
  }
  ul {
    list-style-type: none;
    padding: 0;
  }
  li {
    display: inline-block;
    margin: 0 10px;
  }
  a {
    color: #42b983;
  }
  .image-data {
    border: 1px solid #cccccc;
    float: left;
    width: calc(100% - 332px);
    height: 448px;
    overflow-y: scroll;
  }
  table {
    width: 100%;
    tr {
      td, th {
        border: 1px solid #cccccc;
        padding: 5px;
        margin: 0;
        text-align: left;
      }
    }
  }
  .border {
    border: 1px solid #cccccc;
    background-image: url('../../assets/no-image.png');
    background-repeat: no-repeat;
    background-position: center;
    width: 298px !important;
  }
  .image-film {
    width: 300px;
    float: left;
    margin: 0 20px 20px 0;
    height: 450px;
  }
  a {
    cursor: pointer;
    text-decoration: none;
  }
  .icon-right {
    float: right;
    cursor: pointer;
  }
  .btn-download {
    background: #0095ff;
    padding: 5px 20px 4px 20px;
    font-size: 11px;
    text-align: center;
    color: #ffffff;
    border-radius: 3px;
  }
</style>

filmCardDetail.vue

<template>
  <ul v-show="show">
    <div :class="dataDetailFilm.Poster !== 'N/A' ? 'image-film' : 'image-film border'">
      <img :src="dataDetailFilm.Poster" alt="">
    </div>
    <div class="image-data">
      <table>
        <tr><th>Title</th><td v-text="dataDetailFilm.Title"></td></tr>
        <tr><th>Year</th><td v-text="dataDetailFilm.Year"></td></tr>
        <tr><th>Rated</th><td v-text="dataDetailFilm.Rated"></td></tr>
        <tr><th>Released</th><td v-text="dataDetailFilm.Released"></td></tr>
        <tr><th>Runtime</th><td v-text="dataDetailFilm.Runtime"></td></tr>
        <tr><th>Genre</th><td v-text="dataDetailFilm.Genre"></td></tr>
        <tr><th>Director</th><td v-text="dataDetailFilm.Director"></td></tr>
        <tr><th>Writer</th><td v-text="dataDetailFilm.Writer"></td></tr>
        <tr><th>Actors</th><td v-text="dataDetailFilm.Actors"></td></tr>
        <tr><th>Plot</th><td v-text="dataDetailFilm.Plot"></td></tr>
        <tr><th>Language</th><td v-text="dataDetailFilm.Language"></td></tr>
        <tr><th>Country</th><td v-text="dataDetailFilm.Country"></td></tr>
        <tr><th>Awards</th><td v-text="dataDetailFilm.Awards"></td></tr>
        <tr>
          <th>Ratings</th>
          <td>
            <tr>
              <td v-for="(rating, index) in dataDetailFilm.Ratings" :key="index">
                <b>Source:</b>&nbsp;{{rating.Source}}<br>
                <b>Value:</b>&nbsp;{{rating.Value}}<br>
              </td>
            </tr>
          </td>
        </tr>
        <tr><th>Metascore</th><td v-text="dataDetailFilm.Metascore"></td></tr>
        <tr><th>imdbRating</th><td v-text="dataDetailFilm.imdbRating"></td></tr>
        <tr><th>imdbVotes</th><td v-text="dataDetailFilm.imdbVotes"></td></tr>
        <tr><th>imdbID</th><td v-text="dataDetailFilm.imdbID"></td></tr>
        <tr><th>Type</th><td v-text="dataDetailFilm.Type"></td></tr>
        <tr><th>DVD</th><td v-text="dataDetailFilm.DVD"></td></tr>
        <tr><th>BoxOffice</th><td v-text="dataDetailFilm.BoxOffice"></td></tr>
        <tr><th>Production</th><td v-text="dataDetailFilm.Production"></td></tr>
        <tr><th>Website</th><td><a :href="dataDetailFilm.Website" target="_blank">{{dataDetailFilm.Website}}</a></td></tr>
      </table>
    </div>
  </ul>
</template>
<script>
  export default {
    props: ['dataDetailFilm','showDetailFilm'],
    data () {
      return {
        showDetail: false
      }
    },
    computed: {
      show () {
        this.showDetail ? true : false;
      }
    }
  }
</script>
<style scoped>
</style>

De momento se ha separado parte del código que estaba en App.vue para generar los componentes, pero no esta funcionando todo porque aun quedan conceptos por explicar y que se verán en las próximas entradas.