Programación usando VueJS (Ampliando Vue). Parte 8

Ampliando las capacidades de Vue

En esta nueva entrada vamos a ampliar las capacidades nativas de Vue usando los modifiers, filtros, directivas personalizadas mixins.

Que son los modifiers

Son atributos que se pueden aplicar sobre directivas y que nos permiten extender su funcionalidad.

Dentro de los modifiers tenemos de diferentes tipos:

  • Event modifiers
  • Key modifiers
  • System modifiers

Los modificadores de eventos (Event Modifiers) nos permiten separar ciertos detalles del evento DOM frente a la lógica aplicada al propio evento.

Usando javascript sin ayudarnos de los modificadores de eventos nuestro código sería así:

Javascript

<script>
  showMessage = e => { 
    e.preventDefault();
    alert('msn'); 
  }
</script>
<input type="button" onclick="showMessage(e)" />

Vue

<script>
  showMessage = () => { 
    alert('msn'); 
  }
</script>
<input type="button" @click.prevent="showMessage" />

A continuación los modificadores de eventos:

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive

Debemos tener en cuenta que el orden de los modificadores de eventos afecta al comportamiento que estos tendrán.

Por ejemplo:

  • @click.prevent.self: Evita todos los clics
  • @click.self.prevent: Solo evita los clics en el propio elemento

Los modificadores clave (key modifiers) nos permiten escuchar que tecla es la que permitirá la ejecución.

Estos modificadores nos permiten usar un amplio listado de valores, como .enter, .tab, .space, .down, .right, etc.., usar kebab-case para otros valores como .page-down, .page-up e inclusive usar el valor del keycode.

Por último tenemos los modificadores de sistema (system modifiers), que no son mas que los valores .ctrl, .alt, .shift .meta.

Para probar los modificadores vamos a modificar el código de nuestro proyecto.

home.vue

<template>
  <main v-cloak>
    <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="search-internet" v-show="showSearchInternet">
        <iframe :src="queryComplete" frameborder="0"></iframe>
      </div>
      <div class="box-result">
        <film-card v-show="showFilm" :data-film="dataFilm" :no-image="noImage" @search-in-wikipedia="searchInfoWikipedia">
          <!-- slot con nombre -->
          <h1 slot="busqueda">
            Buscando.... {{search}}
          </h1>
        </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>
  </main>
</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 {
      showButton: false,
      search: '',
      dataFilm: '',
      showError: false,
      msnError: '',
      dataDetailFilm: '',
      showPagination: false,
      pagination: {
        pagActual: 1,
        totalRecords: 0,
        totalPages: 0
      },
      showloading: false,
      noImage: './dist/no-image.png',
      searchInternet: 'https://es.wikipedia.org/wiki/',
      showSearchInternet: false,
      query: ''
    }
  },
  created() {
    this.$bus.$on('show-loading', value => this.showloading = value)
    this.$bus.$on('set-data-film', value => this.dataFilm = value)
    this.$bus.$on('show-error', value => this.showError = value)
    this.$bus.$on('msn-error', value => this.msnError = value)
    this.$bus.$on('data-detail-film', value => this.dataDetailFilm = value)
    this.$bus.$on('show-pagination', value => this.showPagination = value)
  },
  methods: {
    searchInfoWikipedia (title) {
      this.query = `${this.searchInternet}${title}`
      this.showSearchInternet = true
    },
    searchFilm () {
      this.showSearchInternet = false
      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}`)
        })
    },
    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: {
    queryComplete () {
      return this.query
    },
    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>

Continuamos ahora con otra de las características que ofrece Vue, los filtros.

Que son los filtros

Los filtros nos permiten cambiar o visualizar un valor sin modificar su contenido real.

Se pueden crear de forma local o global, y dependiendo del uso que vayamos a dar se define de forma diferente.

Para crear un filtro de forma global lo primero que haremos sera crear una carpeta (por ejemplo filters) dentro de src.

Allí crearemos un fichero que tendrá nuestro filtro, por ejemplo filters.js:

filters.js

const filters = {}

filters.install = Vue => {
  Vue.filter('capitals-decorate-all-words', value => {
    let result = ''
    if (value && value.length > 0) {
      const sp = value.split(' ')
      result = `...:: ${sp.map(data => `${data[0].toUpperCase()}${data.slice(1)}`).join(' ')} ::...`
    }
    return result
  })
}

export default filters

Con nuestro filtro creado ahora lo importamos dentro de nuestro main.js

main.js

import Vue from 'vue'
// Importamos Vue router
import VueRouter from 'vue-router'
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'
import EventBus from '@/plugin/eventBus'
import Filters from '@/filters/filters'

// Importamos las rutas
import routes from '@/routes'

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

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

/*
 * Vue.use nos sirve para usar plugins, librerias externas, etc...
 */
Vue.use(EventBus)
Vue.use(VueRouter)
Vue.use(Filters)

// Declaramos una instancia de Vue-router con las rutas
const router = new VueRouter({
  routes,
  mode: 'history'
})

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

// Pasamos Vuerouter a la instancia global
new Vue({
  el: '#app',
  render: h => h(App),
  router
})

Y para finalizar solo nos queda usar nuestro filtro donde necesitemos, siempre dentro de una expresión y usando el caracter pipe ‘|‘.

filmCardDetail.vue

h>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>


Para terminar con los filtros definiremos uno de forma local, y para ello debemos declararlo dentro del ámbito del componente.

Ejemplo

dateFormat(value) {
    return value && value.length > 0 ? value.split(' ').join('-') : ''
}

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>

Directivas personalizadas

La directivas personalizadas (custom directives) son una funcionalidad que ofrece Vue por defecto y nos permite manipular el DOM de una manera simple.

Aunque Vue trae algunas directivas por defecto como v-if, v-else o v-show entre otras, en ocasiones esas directivas no cubren todas nuestras necesidades.

Por ese motivo Vue ofrece un mecanismo para que nosotros podamos crear nuestras propias directivas.

Como ocurre con los filtros, las directivas personalizadas se pueden declarar de forma global y/o local.

Lo primero que haremos sera crear una carpeta llamada directives dentro de src.

Dentro de la carpeta crearemos un archivo llamado without-image.js, y sera donde esta definido nuestro filtro.

Antes de comenzar a escribir el código para crear nuestra directiva, debemos saber que las directivas tienen unos hooks que se ejecutan de la siguiente forma:

  • bind: Solo se llama una vez, cuando la directiva se enlaza por primera vez con el elemento.
  • inserted: Se llama cuando el elemento enlazado se ha insertado en su nodo principal (esto solo garantiza la presencia del nodo principal, no necesariamente en el documento).
  • update: Se llama después de que el VNodo del componente que contiene se haya actualizado, pero posiblemente antes de que sus hijos se hayan actualizado. El valor de la directiva puede haber cambiado o no, pero puede omitir actualizaciones innecesarias al comparar los valores actuales y antiguos del enlace.
  • componentUpdated: Se llama después de que el VNodo del componente que contiene y los VNodos de sus hijos se hayan actualizado.
  • unbind: Se llama solo una vez, cuando la directiva no está vinculada al elemento.

Teniendo claro los hooks que tienen nuestras directivas personalizadas continuamos con nuestro código.

without-image.js

const withoutImage = {}

const image = (el, binding, newNode, oldNode) => {
  el.src = binding.value === 'N/A' ? 'noImage' : binding.value
}

withoutImage.install = Vue => {
  Vue.directive('without-image', {
    bind (el, binding) {
      image(el, binding)
    }
  })
}

export default withoutImage

Con nuestra directiva creada, ahora vamos a usarla de forma global añadiéndola en nuestro main.js.

main.js

import Vue from 'vue'
// Importamos Vue router
import VueRouter from 'vue-router'
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'
import EventBus from '@/plugin/eventBus'
import Filters from '@/filters/filters'
import DirectiveWithoutImage from '@/directives/without-image'

// Importamos las rutas
import routes from '@/routes'

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

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

/*
 * Vue.use nos sirve para usar plugins, librerias externas, etc...
 */
Vue.use(EventBus)
Vue.use(VueRouter)
Vue.use(Filters)
Vue.use(DirectiveWithoutImage)

// Declaramos una instancia de Vue-router con las rutas
const router = new VueRouter({
  routes,
  mode: 'history'
})

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

// Pasamos Vuerouter a la instancia global
new Vue({
  el: '#app',
  render: h => h(App),
  router
})

Para terminar, añadiremos nuestra directiva en el código para comprobar su funcionamiento.

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>

A continuación podemos ver nuestro código y comparar como queda.

sin directiva

<div :class="film.Poster !== 'N/A' ? 'image-film' : 'image-film border'">
   <img :src="film.Poster !== 'N/A' ? film.Poster : noImage" alt="">
</div>

con directiva

<div :class="film.Poster !== 'N/A' ? 'image-film' : 'image-film border'">
    <img v-without-image="film.Poster" alt="">
</div>

Cuando usamos una directiva el código queda mas limpio y lo podemos usar en cualquier parte.

Como dijimos al principio, también podemos crear un directiva local de una manera muy simple.

Igual que ocurría con los filtros, deberemos crear la directiva dentro del ámbito del componente.

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>

Por último y para terminar esta entrada veremos lo que son los mixins.

Que es un mixin

Los mixins son una forma de distribuir código reutilizable para los componentes de Vue.

Un objeto mixin puede contener cualquier opción del componente, ya que estos se mezclan con las propias opciones del componente.

Como Vue mezcla el código del mixin con el código del componente, si tuviesen el mismo código tendría preferencia el del componente.

Empecemos creando una carpeta llamada mixins dentro de src, y allí dentro crearemos nuestro mixin.

Una vez dentro de la carpeta mixins, crearemos un archivo llamado title.js.

title.js

const mixin = {
  methods: {
    coverBorders (value) {
      debugger;
      return value !== 'N/A' ? 'image-film' : 'image-film border'
    }
  }
}

export default mixin

Para nuestro proyecto hemos creado este pequeño mixin que comprueba si la ficha tiene imagen y añade una clase css o no.

Lo siguiente que haremos es importar nuestro mixin y añadirlo en el componente donde lo vayamos a usar.

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>

Como hemos podido ver, su uso es muy sencillo y nos permite reutilizar el código común.