Cuando crea componentes para su proyecto, todo comienza divertido y fácil.MyButton.vue
Añade un poco de estilo y ya está.
<template>
<button class="my-fancy-style">
<slot></slot>
</button>
</template>
Entonces te das cuenta de inmediato de que necesitas una docena de props, porque tu equipo de diseño quiere que sea de diferentes colores y tamaños, con iconos a la izquierda y a la derecha, con contadores...
const props = withDefaults(defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg; // I’ve described how I cook icons in my previous article
rightIcon?: IconSvg;
counter?: number;
}>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined
});
Después de todo, no puedes tener los botones “Cancelar” y “Ok” del mismo color, y los necesitas para reaccionar a las interacciones de los usuarios.
const props = withDefaults(defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined
});
Bueno, tienes la idea: habrá algo salvaje, como pasarwidth: 100%
o agregando autofocus - todos sabemos cómo es simple en Figma hasta que la vida real golpee duro.
Ahora imagina un botón de enlace: parece el mismo, pero cuando lo presiona, debe ir al enlace externo o interno.<RouterLink>
o<a>
etiquetas cada vez, pero por favor no. También puede agregarto
yhref
Propiedades a su componente inicial, pero se sentirá asfixiado muy pronto:
<component
:is="to ? RouterLink : href ? 'a' : 'button'"
<!-- ugh! -->
Por supuesto, necesitará un componente de "segundo nivel" que envuelva su botón (también manejará los enlaces hipervínculos predeterminados y algunas otras cosas interesantes, pero los omitiré por motivos de simplicidad):
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="$attrs">
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
import MyButton from './MyButton.vue';
import { RouteLocationRaw, RouterLink } from 'vue-router';
const props = defineProps<{
to?: RouteLocationRaw;
href?: string;
}>();
</script>
Y aquí es donde comienza nuestra historia.
Square One
Plaza UnoBueno, básicamente va a funcionar, no voy a mentir.<MyLinkButton :counter=“2">
Pero no habrá autocomplete para los complementos derivados, lo que no es cool:
Podemos propagar props en silencio, pero el IDE no sabe nada sobre ellos, y eso es una vergüenza.
La solución simple y obvia es propagarlos explícitamente:
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton
:theme="props.theme"
:small="props.small"
:icon="props.icon"
:right-icon="props.rightIcon"
:counter="props.counter"
:disabled="props.disabled"
:loading="props.loading"
>
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
// imports...
const props = withDefaults(
defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
to?: RouteLocationRaw;
href?: string;
}>(),
{
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
}
);
</script>
El IDE tendrá el autocomplete adecuado.Tendremos mucho dolor y lamento apoyarlo.
Obviamente, el principio de “No se repita a sí mismo” no se aplicó aquí, lo que significa que tendremos que sincronizar cada actualización. Un día, tendrás que añadir otro prop, y tendrás que encontrar cada componente que envuelva el básico. Sí, el botón y LinkButton son probablemente suficientes, pero imagina TextInput y una docena de componentes que dependen de él: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput.
Después de todo, es feo.Y cuanto más props tenemos, más feo se vuelve.
Clean It Up
Limpia lo de arribaEs bastante difícil reutilizar un tipo anónimo, por lo que le damos un nombre.
// MyButton.props.ts
export interface MyButtonProps {
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}
No podemos exportar una interfaz de la.vue
archivo debido a algún internoscript setup
magia, por lo que necesitamos crear una.ts
En el lado brillante, mira lo que tenemos aquí:
const props = withDefaults(defineProps<MyButtonProps>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
});
Mucho más limpio, ¿no es? y aquí está el heredado:
interface MyLinkButtonProps {
to?: RouteLocationRaw;
href?: string;
}
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
Sin embargo, aquí hay un problema: ahora, cuando los complementos básicos se tratan comoMyLinkButton
de los propósitos, no se propagan conv-bind=”$attrs”
Ya no, así que tenemos que hacerlo nosotros mismos.
<!-- MyLinkButton.vue -->
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="props"> <!-- there we go -->
<slot></slot>
</MyButton>
</component>
Es todo bueno, pero pasamos un poco más de lo que queremos:
Como podéis ver, ahora nuestro botón subyacente también tiene unhref
No es una tragedia, solo un poco de desorden y bytes adicionales, aunque no sea cool.
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="propsToPass">
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
// imports and definitions…
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) => !["to", "href"].includes(key))
)
);
</script>
Ahora, solo pasamos lo que tiene que pasar, pero todas esas letras de cuerdas no parecen increíbles, ¿no? y esa es la historia más triste de TypeScript, chicos.
Interfaces vs Abstract Interfaces
Interfaces versus interfaces abstractasSi alguna vez ha trabajado con los idiomas adecuados orientados a objetos, probablemente conozca cosas como:ReflexionesDesafortunadamente, en TypeScript, las interfaces son efímeras; no existen en tiempo de ejecución, y no podemos averiguar fácilmente qué campos pertenecen a laMyButtonProps
.
Esto significa que tenemos dos opciones.En primer lugar, podemos mantener las cosas como son: cada vez que añadimos unMyLinkButton
También hay que excluirlo depropsToPass
(Y incluso si lo olvidamos, no es una gran cosa).
La segunda manera es usar objetos en lugar de interfaces. Puede sonar sin sentido, pero déjame codificar algo: no será horrible; prometo.quehorrorífico .
// MyButton.props.ts
export const defaultMyButtonProps: MyButtonProps = {
theme: ComponentTheme.BLUE,
small: false,
icon: undefined,
rightIcon: undefined,
counter: undefined,
disabled: false,
loading: false,
};
No tiene sentido crear un objeto sólo para crear un objeto, pero podemos usarlo para los props predeterminados.MyButton.vue
Se vuelve más limpio:
const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);
Ahora solo tenemos que actualizarpropsToPass
enMyLinkButton.vue
:
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) =>
Object.hasOwn(defaultMyButtonProps, key)
)
)
);
Para realizar este trabajo, necesitamos definir explícitamente todos losundefined
ynull
campos endefaultMyButtonProps
De lo contrario, el objeto no “tiene su propio”.
De esta manera, cada vez que añadas un propósito al componente básico, también tendrás que añadirlo al objeto con valores predeterminados. Así que sí, hay dos lugares de nuevo, y tal vez no sea mejor que la solución del capítulo anterior.
I’m Done
Estoy hechoNo es una obra maestra, pero es probablemente lo mejor que podemos hacer dentro de las limitaciones de TypeScript.
También parece que tener tipos de prop dentro del archivo SFC es mejor, pero no puedo decir que moverlos a un archivo separado lo hizo mucho peor.
Puedes encontrar el código de este artículo en GitHub.
GitHub