Declaración de componentes

Componentes de un Solo archivo (alias — SFC) - Más común

<template>
  <p class="demo">
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
};
</script>

<style scoped>
.btn-primary {
  display: inline-block;
  font-size: 1.2rem;
  color: #fff;
  background-color: #3eaf7c;
  padding: 0.8rem 1.6rem;
  border-radius: 4px;
  transition: background-color 0.1s ease;
  box-sizing: border-box;
  border-bottom: 1px solid #389d70;
}
</style>

Plantilla de cadena de texto

Vue.component('my-btn', {
  template: `
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  `,
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
});

Función de renderizado

Vue.component('my-btn', {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
  render(h) {
    return h(
      'button',
      {
        attrs: {
          class: 'btn-primary',
        },
        on: {
          click: this.handleClick,
        },
      },
      this.$slots.default
    );
  },
});

JSX

Vue.component('my-btn', {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
  render() {
    return (
      <button class="btn-primary" @click.prevent="handleClick">
        {this.$slots.default}(clicked - {{count}})
      </button>
    );
  },
});

vue-class-component

<template>
  <button class="btn-primary" @click.prevent="handleClick">
    <slot></slot>(clicked - {{ count }})
  </button>
</template>

<script>
import Vue from 'vue';
import Component from 'vue-class-component';

@Component
export default MyBtn extends Vue {
  count = 0;

  handleClick() {
    this.count++;
    console.log('clicked', this.count);
  }
}
</script>

<style scoped>
.btn-primary {
  background-color: blue;
}
</style>

Referencias:

Comunicación de componentes

Propiedades y Eventos

Básicamente, los componentes de vue siguen un flujo en un sólo sentido, esto es propiedades hacia los hijos (Ver la guía oficial) y eventos hacia el padre. Las props son información de solo lectura, así que es imposible cambiar las props desde un componente hijo. Cuando las props cambian, los componentes hijo serán renderizados de nuevo automáticamente (las props son una fuente de información reactiva). Los componentes hijo solo pueden emitir eventos a sus padres directos, de tal manera que el componente padre puede cambiar su data, que está asignada a las props del componente hijo.

<template>
  <button @click="$emit('click');">{{ text }}</button>
</template>

<script>
export default {
  name: 'v-btn',
  props: {
    text: String,
  },
};
</script>
<template>
  <v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>

<script>
export default {
  data() {
    return {
      clickCount: 0,
      buttonText: 'initial button text',
    };
  },
  methods: {
    handleClick() {
      this.buttonText = `Button clicked ${++this.clickCount}`;
      console.log('clicked', this.buttonText);
    },
  },
};
</script>

Referencias:

Eventos personalizados

Referencias:

Renderización Condicional de Componentes

Directivas (v-if / v-else / v-else-if / v-show)

v-if

<h1 v-if="true">Render only if v-if condition is true</h1>

v-if y v-else

<h1 v-if="true">Render only if v-if condition is true</h1>
<h1 v-else>Render only if v-if condition is false</h1>

v-else-if

<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
<div v-else>Render if `type` is not `A` or `B` or `C`</div>

v-show

<h1 v-show="true">
  Always rendered, but it should be visible only if `v-show` conditions is true
</h1>

Si necesitas renderizar condicionalmente más de un elemento, puedes usar las directivas (v-if / v-else / v-else-if /v-show) en un elemento <template>. Ten en cuenta que el elemento <template> no se renderiza en el DOM. Es un contenedor invisible.

<template v-if="true">
  <h1>All the elements</h1>
  <p>will be rendered into DOM</p>
  <p>except `template` element</p>
</template>

Función de Renderizado o JSX

Si usas funciones de renderizado o JSX en tu aplicación vue, puedes aplicar todas las técnicas, tales como declaraciones if else y switch case y operadores ternarios y logicos.

Declaración if else

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    if (this.isTruthy) {
      return <h1>Render value is true</h1>;
    } else {
      return <h1>Render value is false</h1>;
    }
  },
};

Declaración switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    switch (this.type) {
      case 'info':
        return <Info text={text} />;
      case 'warning':
        return <Warning text={text} />;
      case 'error':
        return <Error text={text} />;
      default:
        return <Success text={text} />;
    }
  },
};

o puedes utilizar un objeto diccionario para simplificar el switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

const COMPONENT_MAP = {
  info: Info,
  warning: Warning,
  error: Error,
  success: Success,
};

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    const Comp = COMPONENT_MAP[this.type || 'success'];

    return <Comp />;
  },
};

Operador ternario

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    return (
      <div>
        {this.isTruthy ? (
          <h1>Render value is true</h1>
        ) : (
          <h1>Render value is false</h1>
        )}
      </div>
    );
  },
};

Operador lógico

export default {
  data() {
    return {
      isLoading: true,
    };
  },
  render(h) {
    return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
  },
};

Referencias

Componentes Dinámico

<component> con atributo is

<component :is="currentTabComponent"></component>

Con el ejemplo de código de arriba, el componente renderizado será destruido si un componente diferente es asignado en la propiedad is. Si deseas que los componentes mantengan sus instancias y su estado sin ser destruido, puedes envolver la etiqueta <component> en otra etiqueta <keep-alive> de la siguiente manera:

<keep-alive> <component :is="currentTabComponent"></component> </keep-alive>

Referencias

Componente Funcional

Un componente funcional es un SFC especial, es básicamente un componente sin estado (es decir, sin etiqueta de script). Solamente acepta props para mostrar datos.

Para poder hacer un SFC un componente funcional, necesitas agregar el atributo functional a la etiqueta de <template> de la siguiente manera <template functional>

fp-component.vue

<template functional>
  <h1>{{ props.title }}</h1>
  <p>{{ props.description }}</p>
</template>

index.vue

<template>
  <fp-component v-bind="{ title: 'FP Component', description: 'Only takes props' }" />
</template>

<script>
import FPComponent from './fp-component';

export default {
  components: {
    FPComponent
  }
}
</script>

Beneficios de usar un Componente Funcional sobre un Componente con Estado:

  • Renderizado más rápido
  • Menor uso de memoria

Componente sin renderizado

Un componente sin renderizado es básicamente un componente que no renderiza ningún HTML en el DOM pero proporciona lógica JavaScript reutilizable abstraída en un SFC.

Un componente sin renderizado utiliza la API de Slots para lograr lo que queremos.

Renderless.vue

<script>
export default {
  render() {
    return this.$scopedSlots.default({ name: 'John' });
  }
};
</script>

El único trabajo de Renderless.vue es proporcionar la prop name

App.vue

<template>
  <renderless v-slot="{ name }">
    <p>{{ name }}</p>
  </renderless>
</template>

<script>
import Renderless from './Renderless.vue';

export default {
  components: {
    Renderless,
  }
};
</script>

Lo bueno de utilizar Componentes sin renderizado es que nos permite separar nuestra lógica del HTML.

Composición

Librería

Composición básica

<template>
  <div class="component-b"><component-a></component-a></div>
</template>

<script>
import ComponentA from './ComponentA';

export default {
  components: {
    ComponentA,
  },
};
</script>

Referencias

Extends

Cuando quieres extender un solo componente de vue

<template>
  <button class="button-primary" @click.prevent="handleClick">
    {{ buttonText }}
  </button>
</template>

<script>
import BaseButton from './BaseButton';

export default {
  extends: BaseButton,
  props: ['buttonText'],
};
</script>

References:

Mixins

// closableMixin.js
export default {
  props: {
    isOpen: {
      default: true,
    },
  },
  data: function() {
    return {
      shown: this.isOpen,
    };
  },
  methods: {
    hide: function() {
      this.shown = false;
    },
    show: function() {
      this.shown = true;
    },
    toggle: function() {
      this.shown = !this.shown;
    },
  },
};
<template>
  <div
    v-if="shown"
    class="alert alert-success"
    :class="'alert-' + type"
    role="alert"
  >
    {{ text }}
    <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
  </div>
</template>

<script>
import closableMixin from './mixins/closableMixin';

export default {
  mixins: [closableMixin],
  props: ['text'],
};
</script>

Referencias:

2.6.0+

Si usas Vue en la versión 2.6.0 o mayor, Vue introdujo una nueva y unificada api para slots, que es v-slot. Reemplaza los atributos slot y slot-scope, que son obsoletos, pero no han sido removidos y se encuentran todavía documentados aquí. Puedes hacer referencia la API obsoleta aquí.

Slots (Por defecto)

<template>
  <button class="btn btn-primary">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'VBtn',
};
</script>
<template>
  <v-btn>
    <span class="fa fa-user"></span> Login
  </v-btn>
</template>

<script>
import VBtn from './VBtn';

export default {
  components: {
    VBtn,
  },
};
</script>

References:

Slots Nombrados

BaseLayout.vue

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

App.vue

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

Vue proporciona una sintaxis abreviada para slots nombrados. Puedes remplazar v-slot: con #

Referencias

Slots con Scope

<template>
  <ul>
    <li v-for="todo in todos" v-bind:key="todo.id">
      <!-- We have a slot for each todo, passing it the -->
      <!-- `todo` object as a slot prop. -->
      <slot v-bind:todo="todo"> {{ todo.text }} </slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    todos: {
      type: Array,
      default: () => [],
    },
  },
};
</script>
<template>
  <todo-list v-bind:todos="todos">
    <template v-slot:default="{ todo }">
      <span v-if="todo.isComplete"></span>
      {{ todo.text }}
    </template>
  </todo-list>
</template>

<script>
import TodoList from "./TodoList";

export default {
  components: {
    TodoList
  },
  data() {
    return {
      todos: [
        { text: "todo 1", isComplete: true },
        { text: "todo 2", isComplete: false },
        { text: "todo 3", isComplete: false },
        { text: "todo 4", isComplete: true }
      ]
    };
  }
};
</script>

Referencias:

Props de Renderizado

En la mayoría de los casos, puedes usar slots con scope en vez de props de renderizado. Pero, puede llegar a ser util en algunas ocasiones.

con SFC

<template>
  <div id="app"><Mouse :render="__render" /></div>
</template>

<script>
import Mouse from './Mouse.js';
export default {
  name: 'app',
  components: {
    Mouse,
  },
  methods: {
    __render({ x, y }) {
      return (
        <h1>
          The mouse position is ({x}, {y})
        </h1>
      );
    },
  },
};
</script>
<style>
* {
  margin: 0;
  height: 100%;
  width: 100%;
}
</style>

con JSX

const Mouse = {
  name: 'Mouse',
  props: {
    render: {
      type: Function,
      required: true,
    },
  },
  data() {
    return {
      x: 0,
      y: 0,
    };
  },
  methods: {
    handleMouseMove(event) {
      this.x = event.clientX;
      this.y = event.clientY;
    },
  },
  render(h) {
    return (
      <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
        {this.$props.render(this)}
      </div>
    );
  },
};

export default Mouse;

Referencias:

Pasando Props y Listeners

En ciertas ocasiones, es posible que quieras pasar props y listeners a un componente hijo sin tener que declarar todas las props del componente hijo. Puedes ligar $attrs y $listeners en el componente hijo y establecer inheritAttrs como false (de lo contrario, div y child-component` recibiran los atributos)

PassingProps.vue

<template>
  <div>
    <h1>{{title}}</h1>
    <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
  </div>
</template>

<script>
import PassingPropsChild from './PassingPropsChild';

export default {
  components: {
    PassingPropsChild,
  },
  inheritAttrs: false,
  props: {
    title: {
      type: String,
      default: 'Hello, Vue!',
    },
  },
};
</script>

Desde el componente padre, puedes hacer esto:

PassedProps.vue

<template>
  <p class="demo">
    <passing-props
      title="This is from <passing-props />"
      childPropA="This is from <passing-props-child />"
      @click="handleClickPassingPropsChildComponent"
    >
    </passing-props>
  </p>
</template>

<script>
import PassingProps from './PassingProps';

export default {
  components: {
    PassingProps,
  },
  methods: {
    handleClickPassingPropsChildComponent() {
      console.log('This event comes from `<passing-props-child />`');
      alert('This event comes from `<passing-props-child />`');
    },
  },
};
</script>

Ejemplo:

This is from <passing-props />

Referencias:

Componentes de orden superior (alias HOC)

Referencias:

Proveedor / Consumidor

El patrón Proveedor / Consumidor es muy sencillo, su objetivo es separar la lógica de la presentación. Necesitamos dos componentes para crear este patrón.

Provider.vue

<template>
  <div>
    <slot v-bind="{ state, actions }" />
  </div>
</template>

<script>
export default {
  computed: {
    state() {
      return {
        label: 'button',
      };
    },
    actions() {
      return {
        click: this.click,
      };
    },
  },
  methods: {
    click() {
      console.log('Clicked');
    },
  },
}
</script>

Provider.vue es responsable de contener la lógica de estado, estamos separándola de la presentación. Podemos hacer uso de la API de Slots como proveedor de la información.

Consumer.vue

<template functional>
  <div>
    <p>{{ props.state.label }}</p>
    <button @click="props.actions.click">CLICK</button>
  </div>
</template>

Consumer.vue es responsable de contener la presentación, toma en cuenta que estamos utilizando un Componente Funcional.

App.vue

<template>
  <provider v-slot="{ state, actions }">
    <consumer v-bind="{ state, actions }" />
  </provider>
</template>

<script>
import Provider from './Provider.vue';
import Consumer from './Consumer.vue';

export default {
  components: {
    Provider,
    Consumer,
  },
};
</script>

Este patrón proporciona una buena manera que nos permite componer componentes limpios y desacoplados. Puedes ver los ejemplos en CodeSandbox

Inyección de Dependencia

Vue tiene un mecanismo para proveer / inyectar un objeto a todos sus descendientes, sin importar el nivel de profundidad de la jerarquía del componente, siempre y cuando se encuentre dentro de la misma cadena del padre. Vale mencionar que las ligaduras provide e inject no son reactivas, a menos que pasen un objeto observado.

<parent-component>
  <child-component>
    <grand-child-component></grand-child-component>
  </child-component>
</parent-component>

En el ejemplo de la jerarquía de componentes de arriba, para poder derivar información desde parent-component, necesitas pasar el data(object) como props a child-component y a grand-child-component. Sin embargo, si parent-component utiliza provide con el data(object), el componente grand-child-component puede definir inject para inyectar el objeto que proviene de parent-component.

Referencias:

Provide / Inject

TIP

Puedes usar también @Provide, @Inject de vue-property-decorator

ThemeProvider.vue

<script>
export default {
  provide: {
    theme: {
      primaryColor: '#3eaf7c',
      secondaryColor: '#1FA2FF'
    },
  },
  render(h) {
    return this.$slots.default[0];
  },
};
</script>

ThemeButton.vue

<template>
  <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor)  }">
    <slot></slot>
  </button>
</template>

<script>
export default {
  inject: {
    theme: {
      default: {},
    },
  },
  props: {
    primary: {
      type: Boolean,
      default: false,
    },
    secondary: {
      type: Boolean,
      default: false,
    },
  },
};
</script>
<theme-provider>
  <p class="demo">
    <button class="btn">Normal Button</button>
    <theme-button secondary>Themed Button</theme-button>
  </p>
</theme-provider>

Ejemplo:

Manejando errores

Hook errorCaptured

ErrorBoundary.vue

<script>
export default {
  name: 'ErrorBoundary',
  data() {
    return {
      error: false,
      errorMessage: '',
    };
  },
  errorCaptured(err, vm, info) {
    this.error = true;
    this.errorMessage = `Sorry, error occured in ${info}`;

    return false;
  },
  render(h) {
    if (this.error) {
      return h('p', { class: 'demo bg-danger' }, this.errorMessage);
    }

    return this.$slots.default[0];
  },
};
</script>

ThrowError.vue

<template>
  <p class="demo">
    <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  watch: {
    count() {
      throw new Error('error');
    },
  },
  methods: {
    throwError() {
      this.count++;
    },
  },
};
</script>
<error-boundary> <throw-error></throw-error> </error-boundary>

Ejemplo:

Referencias

Trucos de productividad

watch al crear un componente

// no hagas esto
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}
// mejor haz esto
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}