Components (composition api)
⭐ Videos Premium ⭐
Esta sección es parte del curso en Udemy. Si quieres acceder a ella, puedes comprar el curso en Udemy: Vue.js + Firebase by bluuweb cupón de descuento aplicado 😊.
Archivos del curso
script setup
- doc vue setup
<script setup>
es un azúcar sintáctico en tiempo de compilación para usar la API de composición dentro de componentes de archivo único (SFC).- Es la sintaxis recomendada si está utilizando tanto SFC como API de composición.
- Proporciona una serie de ventajas:
- Código más breve y menos repetitivo.
- Mejor rendimiento en tiempo de ejecución.
- Capacidad para declarar accesorios y eventos emitidos usando TypeScript puro.-
sin script setup
js
import { ref } from "vue";
export default {
// `setup` is a special hook dedicated for composition API.
setup() {
const count = ref(0);
function increment() {
count.value++;
}
// expose the state to the template
return {
state,
increment,
};
},
};
Options API
- Con la API de opciones, usamos la opción data para declarar el estado reactivo de un componente.
- El valor de la opción debe ser una función que devuelva un objeto.
- Vue llamará a la función al crear una nueva instancia de componente y envolverá el objeto devuelto en su sistema de reactividad.
- Cualquier propiedad de nivel superior de este objeto se representa en la instancia del componente ( this en métodos y enlaces de ciclo de vida):
js
export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
},
};
Components
- component
- Los componentes nos permiten dividir la interfaz de usuario en piezas independientes y reutilizables, y pensar en cada pieza de forma aislada.
ButtonCounter.vue
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
App.vue: Los componentes se pueden reutilizar tantas veces como quieras:
vue
<script setup>
import ButtonCounter from "./ButtonCounter.vue";
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
</template>
- Fíjate que al hacer clic en los botones, cada uno mantiene el suyo propio, separado count. Esto se debe a que cada vez que usa un componente, se crea una nueva instancia del mismo.
- En SFC, se recomienda usar nombres PascalCase de etiquetas para componentes secundarios para diferenciarlos de los elementos HTML nativos.
- Si está creando sus plantillas directamente en un DOM, la plantilla estará sujeta al comportamiento de análisis HTML nativo del navegador. En tales casos, deberá usar etiquetas kebab-case de cierre explícitas para los componentes:
html
<!-- if this template is written in the DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
Props (defineProps)
- Los accesorios son atributos personalizados que puede registrar en un componente.
- defineProps: es una macro en tiempo de compilación que solo está disponible en el interior
<script setup>
y no necesita ser importada explícitamente. - Los accesorios declarados se exponen automáticamente a la plantilla.
BlogPost.vue
vue
<script setup>
defineProps(["title"]);
</script>
<template>
<h2>{{ title }}</h2>
</template>
App.vue
html
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
- defineProps también devuelve un objeto que contiene todos los accesorios pasados al componente, para que podamos acceder a ellos en JavaScript si es necesario:
js
const props = defineProps(["title"]);
console.log(props.title);
Si no está usando <script setup>
, los accesorios deben declararse usando la opción props, y el objeto props se pasará setup()como el primer argumento:
js
export default {
props: ["title"],
setup(props) {
console.log(props.title);
},
};
Props objeto
- Esto no solo documenta su componente, sino que también advertirá a otros desarrolladores que usan su componente en la consola del navegador si pasan el tipo incorrecto.
vue
<script setup>
// String, Number, Boolean, Array, Object, Date, Function, Symbol
defineProps({
title: String,
id: Number,
body: {
type: String,
default: "Sin descripción",
},
});
</script>
<template>
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ id }} - {{ title }}</h5>
<p>{{ body }}</p>
</div>
</div>
</template>
App.vue
html
<BlogPost
title="Post 01"
body="Descrión del post 01"
:id="1"
/>
<BlogPost
title="Post 02"
body="Descrión del post 02"
:id="2"
/>
<BlogPost
title="Post 03"
body="Descrión del post 03"
:id="3"
/>
Pros y v-for
vue
<script setup>
import { ref } from "vue";
import BlogPost from "./components/BlogPost.vue";
const posts = ref([
{ id: 1, title: "Post 01", body: "Descrión del post 01" },
{ id: 2, title: "Post 02", body: "Descrión del post 02" },
{ id: 3, title: "Post 03" },
]);
</script>
<template>
<div class="container">
<h1>Blog</h1>
<BlogPost
v-for="post in posts"
:key="post.title"
:title="post.title"
:id="post.id"
:body="post.body"
class="mb-2"
/>
</div>
</template>
Emit (Escuchar eventos)
vue
<script setup>
import { ref } from "vue";
import BlogPost from "./components/BlogPost.vue";
const posts = ref([
{ id: 1, title: "Post 01", body: "Descrión del post 01" },
{ id: 2, title: "Post 02", body: "Descrión del post 02" },
{ id: 3, title: "Post 03" },
]);
const miFavorito = ref("");
const fijarFavorito = (title) => {
miFavorito.value = title;
};
</script>
<template>
<div class="container">
<h1>{{ miFavorito || "Sin favorito" }}</h1>
<div>
<BlogPost
v-for="post in posts"
:key="post.title"
:title="post.title"
:id="post.id"
:body="post.body"
class="mb-2"
@fijarFavorito="fijarFavorito"
/>
</div>
</div>
</template>
BlogPost.vue
vue
<script setup>
defineProps({
title: String,
id: Number,
body: {
type: String,
default: "Sin descripción",
},
});
</script>
<template>
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p>{{ body }}</p>
<button
class="btn btn-sm btn-outline-primary"
@click="$emit('fijarFavorito', title)"
>
Mi Favorito
</button>
</div>
</div>
</template>
defineEmits
vue
<script setup>
defineProps({
title: String,
id: Number,
body: {
type: String,
default: "Sin descripción",
},
});
const emit = defineEmits(["fijarFavorito"]);
</script>
<template>
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p>{{ body }}</p>
<button
class="btn btn-sm btn-outline-primary"
@click="emit('fijarFavorito', title)"
>
Mi Favorito
</button>
</div>
</div>
</template>
setup function:
js
export default {
emits: ["enlarge-text"],
setup(props, ctx) {
ctx.emit("enlarge-text");
},
};
Function Props
- Parece tentador pero analice el flujo de trabajo... aquí le estamos pasando a cada uno de nuestros componentes tooooooda la función, por ende cada posts tendrá este método en su script setup.
- En cambio, cuando activamos un evento (defineEmits), no es que el método exista en el componente, sino que lo estamos llamando del componente principal, donde ahí solo existe un evento en cuestión.
- Por ende esto se considera un antipatrón en Vue.
html
<BlogPost
v-for="post in posts"
:key="post.title"
:title="post.title"
:id="post.id"
:body="post.body"
class="mb-2"
:fijarFavorito="fijarFavorito"
/>
vue
<script setup>
defineProps({
title: String,
id: Number,
body: {
type: String,
default: "Sin descripción",
},
fijarFavorito: Function,
});
// defineProps(["title", "id", "body", "fijarFavorito"]);
</script>
<template>
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p>{{ body }}</p>
<button
class="btn btn-sm btn-outline-primary"
@click="fijarFavorito(title)"
>
Mi Favorito
</button>
</div>
</div>
</template>
Práctica
js
const posts = ref([]);
fetch("https://jsonplaceholder.typicode.com/posts")
.then((res) => res.json())
.then((data) => (posts.value = data));
PaginationPosts.vue
vue
<script setup></script>
<template>
<div
class="btn-group"
role="group"
aria-label="Basic example"
>
<button
type="button"
class="btn btn-outline-primary"
>
Previus
</button>
<button
type="button"
class="btn btn-outline-primary"
>
Next
</button>
</div>
</template>
Solución
App.vue
vue
<script setup>
import { computed } from "@vue/reactivity";
import { ref } from "vue";
import BlogPost from "./components/BlogPost.vue";
import PaginatePost from "./components/PaginatePost.vue";
const miFavorito = ref("");
const posts = ref([]);
const postXpage = 10;
const inicio = ref(0);
const fin = ref(postXpage);
fetch("https://jsonplaceholder.typicode.com/posts")
.then((res) => res.json())
.then((data) => (posts.value = data));
const fijarFavorito = (title) => {
miFavorito.value = title;
};
const next = () => {
inicio.value = inicio.value + postXpage;
fin.value = fin.value + postXpage;
};
const prev = () => {
inicio.value = inicio.value - postXpage;
fin.value = fin.value - postXpage;
};
const maxLength = computed(() => posts.value.length);
</script>
<template>
<div class="container">
<h1>{{ miFavorito || "Sin favorito" }}</h1>
<PaginatePost
@next="next"
@prev="prev"
:inicio="inicio"
:fin="fin"
:maxLength="maxLength"
class="mb-2"
></PaginatePost>
<BlogPost
v-for="post in posts.slice(inicio, fin)"
:key="post.title"
:title="post.title"
:id="post.id"
:body="post.body"
class="mb-2"
@fijarFavorito="fijarFavorito"
>
</BlogPost>
</div>
</template>
PaginatePost.vue
vue
<script setup>
defineProps(["inicio", "fin", "maxLength"]);
const emit = defineEmits(["next", "prev"]);
</script>
<template>
<div
class="btn-group"
role="group"
aria-label="Basic example"
>
<button
type="button"
class="btn btn-outline-primary"
@click="emit('prev')"
:disabled="inicio === 0"
>
Previus
</button>
<button
type="button"
class="btn btn-outline-primary"
@click="emit('next')"
:disabled="fin >= maxLength"
>
Next
</button>
</div>
</template>
Ciclo de vida
- Cómo nos dimos cuenta, utilizar el fetch no lleva ningún problema utilizando setup. Esto pasa que por defecto se ejecuta al momento de la creación de nuestro componente.
- Pero en Vue también podemos utilizar diferentes etapas del ciclo de vida de un componente.
- Más información
LoadingSpinner.vue
vue
<script setup></script>
<template>
<div class="mt-5 text-center">
<div
class="spinner-border text-primary"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</div>
<p class="text-center mt-2">Cargando...</p>
</template>
vue
<script setup>
import { onMounted, ref, computed } from "vue";
import BlogPost from "./components/BlogPost.vue";
import PaginatePost from "./components/PaginatePost.vue";
import LoadingSpinner from "./components/LoadingSpinner.vue";
const miFavorito = ref("");
const posts = ref([]);
const postXpage = 10;
const inicio = ref(0);
const fin = ref(postXpage);
const loading = ref(false);
onMounted(async () => {
loading.value = true;
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
posts.value = await res.json();
} catch (error) {
console.log(error);
} finally {
setTimeout(() => (loading.value = false), 1500);
}
});
const fijarFavorito = (title) => {
miFavorito.value = title;
};
const next = () => {
inicio.value = inicio.value + postXpage;
fin.value = fin.value + postXpage;
};
const prev = () => {
inicio.value = inicio.value - postXpage;
fin.value = fin.value - postXpage;
};
const maxLength = computed(() => posts.value.length);
const paginatePage = computed(() => posts.value.slice(inicio.value, fin.value));
</script>
<template>
<LoadingSpinner v-if="loading" />
<div
class="container"
v-else
>
<h1>{{ miFavorito || "Sin favorito" }}</h1>
<!-- <p>{{ inicio }} - {{ fin }}</p> -->
<PaginatePost
@next="next"
@prev="prev"
:inicio="inicio"
:fin="fin"
:maxLength="maxLength"
class="mb-2"
></PaginatePost>
<BlogPost
v-for="post in paginatePage"
:key="post.title"
:title="post.title"
:id="post.id"
:body="post.body"
class="mb-2"
@fijarFavorito="fijarFavorito"
>
</BlogPost>
</div>
</template>