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:
- 🇪🇸 Documentación - Componentes de un Solo archivo
- 🇪🇸 Documentación - Funciones de renderizado y JSX
- 🇺🇸 7 Maneras de definir una plantilla de componente en VueJS
- 🇺🇸 Escribir múltiple componentes de Vue en un solo archivo
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:
- 🇪🇸 Documentación - Propiedades
- 🇺🇸 Patrones de comunicacion de componentes Vue.js
- 🇺🇸 Crear controles de entrada personalizados con Vue.js
- 🇺🇸 Comunicación de componentes hermanos en Vue
- 🇺🇸 Manejar el Estado en Vue.js
- 🇺🇸 Comunicación de Vue.js parte 2: componentes padre-hijo
- 🇺🇸 Patrones de diseño para comunicación entre componentes de Vue.js
Eventos personalizados
Referencias:
- 🇪🇸 Documentación - Eventos personalizados
- 🇺🇸 Aprovechar los eventos de Vue para reducir declaraciones de propiedades
- 🇺🇸 Hooks de componentes de Vue.js como Eventos
- 🇺🇸 Crear un Bus de eventos global con Vue.js
- 🇺🇸 Bus de eventos de Vue.js + Promesas
- 🇺🇸 Modificar la data de un componente con emisores de eventos en Vue.js
Renderización Condicional de Componentes
v-if
/ v-else
/ v-else-if
/ v-show
)
Directivas (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
- 🇪🇸 Documentación - Componentes Dinámicos
- 🇪🇸 Documentación - Componentes Dinámicos & Asíncronos
- 🇺🇸 Plantillas de Componentes Dinámicos con Vue.js
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:
- 🇪🇸 Documentación - Contenido del slot
- 🇺🇸 Entendiendo los Slots en componentes con Vue.js
- 🇺🇸 Componiendo Elementos Personalizados con Slots y Slots nombrados
- 🇺🇸 Escribiendo Components Abstractos con Vue.js
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:
- 🇪🇸 Documentación - Slots con Scope
- 🇺🇸 Comprende de pies a cabeza los Slots con Scope en Vue.js
- 🇺🇸 Entendiendo Slots con Scope en Vue.js
- 🇺🇸 Componentes y Slots con Scope en Vue.js
- 🇺🇸 El Truco para entender Slots con Scope en Vue.js
- 🇺🇸 El poder de Slots con Scope en Vue
- 🇺🇸 Construye un Componente lista con Vue.js y Slots con Scope
- 🇺🇸 Cómo finalmente comprendí los Slots con Scope en Vue
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:
- 🇪🇸 Documentación - Funciones de renderizado y JSX
- 🇺🇸 Aprovechando Props de Renderizado en Vue
- 🇺🇸 Utilizando Callback Props estilo React con Vue: Pros y Contras
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:
- 🇺🇸 Componentes de orden superior en Vue.js
- 🇺🇸 ¿Son necesarios los Componentes de orden superior en Vue.js?
- 🇺🇸 Componentes de orden superior en Vue.js
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:
- 🇪🇸 Documentación - Provide / Inject
- 🇪🇸 Documentación- Inyección de dependencia
- 🇺🇸 Comunicación de Componentes
- 🇺🇸 Inyección de dependencia en una aplicación Vue.js App con TypeScript
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
errorCaptured
Hook 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,
}
}