El componente Radio de Vue3 permite seleccionar una opción entre varias. Se puede personalizar mediante las siguientes propiedades:
opciones(Option[]): arreglo de objetos conetiquetayvalor. Por defecto:[].deshabilitado(boolean): deshabilita todo el grupo. Por defecto:false.vertical(boolean): disposición vertical de las opciones. Solo aplica sibotonesfalse. Por defecto:false.modelValueCheck(v-model:checked, boolean): controla el estado seleccionado del radio único. Por defecto:false.espaciado(number | number[]): separación entre radios, en px. Si es vertical, es el espacio vertical. Si es un arreglo: [horizontal, vertical] para cuando hay salto de línea. Solo sibotonesfalse. Por defecto:8.boton(boolean): usa estilo de botón. Por defecto:false.estiloBoton('outline' | 'solid'): estilo del botón (solo sibotonestrue). Por defecto:'outline'.tamanioBoton('small' | 'middle' | 'large'): tamaño del botón (solo sibotonestrue). Por defecto:'middle'.modelValue(v-model:value, string | number | boolean): valor seleccionado actual. Por defecto:undefined.
Creación del componente Radio.vue
El siguiente código implementa el componente con las funcionalidades descritas. Utiliza las utilidades useSlotsExist y useInject (no mostradas aquí).
<script lang="ts" setup>
import { computed, ref, watchEffect, nextTick } from 'vue'
import { useSlotsExist, useInject } from 'components/utils'
export interface Opcion {
etiqueta: string
valor: string | number | boolean
deshabilitado?: boolean
}
export interface Props {
opciones?: Opcion[]
deshabilitado?: boolean
vertical?: boolean
modelValueCheck?: boolean
espaciado?: number | number[]
boton?: boolean
estiloBoton?: 'outline' | 'solid'
tamanioBoton?: 'small' | 'middle' | 'large'
modelValue?: string | number | boolean
}
const props = withDefaults(defineProps<Props>(), {
opciones: () => [],
deshabilitado: false,
vertical: false,
modelValueCheck: false,
espaciado: 8,
boton: false,
estiloBoton: 'outline',
tamanioBoton: 'middle',
modelValue: undefined
})
const checkSeleccionado = ref<boolean>(false)
const valorSeleccionado = ref<string | number | boolean>()
const ondaActiva = ref<boolean>(false)
const { paletasColor } = useInject('Radio')
const emit = defineEmits(['update:modelValueCheck', 'update:modelValue', 'cambio'])
const slotsExisten = useSlotsExist(['default'])
const cantidadOpciones = computed(() => props.opciones.length)
const valorEspaciado = computed(() => {
if (!props.boton) {
if (!props.vertical && Array.isArray(props.espaciado)) {
return `${props.espaciado[1]}px ${props.espaciado[0]}px`
}
return `${props.espaciado}px`
}
return 0
})
watchEffect(() => {
checkSeleccionado.value = props.modelValueCheck
})
watchEffect(() => {
valorSeleccionado.value = props.modelValue
})
function verificarDeshabilitado(deshabilitado?: boolean): boolean {
return deshabilitado !== undefined ? deshabilitado : props.deshabilitado
}
function alHacerClic(valor: string | number | boolean): void {
if (valor !== valorSeleccionado.value) {
iniciarOnda()
valorSeleccionado.value = valor
emit('update:modelValue', valor)
emit('cambio', valor)
}
}
function alMarcar(): void {
if (!checkSeleccionado.value) {
iniciarOnda()
checkSeleccionado.value = true
emit('update:modelValueCheck', true)
emit('cambio', true)
}
}
function iniciarOnda(): void {
ondaActiva.value = !ondaActiva.value
nextTick(() => {
ondaActiva.value = true
})
}
function terminarOnda(): void {
ondaActiva.value = false
}
</script>
<template>
<div v-if="cantidadOpciones" class="radio-contenedor" :class="{ 'radio-vertical': !boton && vertical }" :style="`--radio-espaciado: ${valorEspaciado}; --color-primario: ${paletasColor[5]};`" v-bind="$attrs">
<template v-if="!boton">
<div v-for="(opc, idx) in opciones" :key="idx" class="radio-item" :class="{ 'radio-deshabilitado': verificarDeshabilitado(opc.deshabilitado) }" @click="verificarDeshabilitado(opc.deshabilitado) ? () => {} : alHacerClic(opc.valor)">
<span class="radio-mango" :class="{ 'radio-checkeado': valorSeleccionado === opc.valor }">
<span v-if="!verificarDeshabilitado(opc.deshabilitado)" class="radio-onda" :class="{ 'onda-activa': ondaActiva && valorSeleccionado === opc.valor }" @animationend="terminarOnda"></span>
</span>
<span class="radio-etiqueta">
<slot :option="opc" :label="opc.etiqueta" :index="idx">{{ opc.etiqueta }}</slot>
</span>
</div>
</template>
<template v-else>
<div v-for="(opc, idx) in opciones" :key="idx" tabindex="0" class="radio-boton" :class="{
'radio-boton-checkeado': valorSeleccionado === opc.valor,
'radio-boton-deshabilitado': verificarDeshabilitado(opc.deshabilitado),
'radio-boton-solido': estiloBoton === 'solid',
'radio-boton-pequeno': tamanioBoton === 'small',
'radio-boton-grande': tamanioBoton === 'large'
}" @click="verificarDeshabilitado(opc.deshabilitado) ? () => {} : alHacerClic(opc.valor)">
<span class="radio-etiqueta">
<slot :option="opc" :label="opc.etiqueta" :index="idx">{{ opc.etiqueta }}</slot>
</span>
<span v-if="!verificarDeshabilitado(opc.deshabilitado)" class="radio-onda" :class="{ 'onda-activa': ondaActiva && valorSeleccionado === opc.valor }" @animationend="terminarOnda"></span>
</div>
</template>
</div>
<template v-else>
<div v-if="!boton" class="radio-item" :class="{ 'radio-deshabilitado': deshabilitado }" :style="`--color-primario: ${paletasColor[5]};`" @click="deshabilitado ? () => {} : alMarcar()" v-bind="$attrs">
<span class="radio-mango" :class="{ 'radio-checkeado': checkSeleccionado }">
<span v-if="!deshabilitado" class="radio-onda" :class="{ 'onda-activa': ondaActiva && checkSeleccionado }" @animationend="terminarOnda"></span>
</span>
<span v-if="slotsExisten.default" class="radio-etiqueta">
<slot></slot>
</span>
</div>
<div v-else tabindex="0" class="radio-boton radio-boton-unico" :class="{
'radio-boton-checkeado': checkSeleccionado,
'radio-boton-deshabilitado': deshabilitado,
'radio-boton-solido': estiloBoton === 'solid',
'radio-boton-pequeno': tamanioBoton === 'small',
'radio-boton-grande': tamanioBoton === 'large'
}" :style="`--color-primario: ${paletasColor[5]};`" @click="deshabilitado ? () => {} : alMarcar()" v-bind="$attrs">
<span class="radio-etiqueta">
<slot></slot>
</span>
<span v-if="!deshabilitado" class="radio-onda" :class="{ 'onda-activa': ondaActiva && checkSeleccionado }" @animationend="terminarOnda"></span>
</div>
</template>
</template>
<style lang="less" scoped>
/* estilos adaptados con nombres de clase modificados */
.radio-contenedor {
display: inline-flex;
flex-wrap: wrap;
gap: var(--radio-espaciado);
}
.radio-vertical {
flex-direction: column;
flex-wrap: nowrap;
}
.radio-item {
display: inline-flex;
align-items: baseline;
cursor: pointer;
color: rgba(0,0,0,0.88);
font-size: 14px;
line-height: 1.571;
&:not(.radio-deshabilitado):hover .radio-mango {
border-color: var(--color-primario);
}
.radio-mango {
flex-shrink: 0;
align-self: center;
position: relative;
width: 16px;
height: 16px;
background: transparent;
border: 1px solid #d9d9d9;
border-radius: 50%;
transition: all 0.3s;
&::after {
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 16px;
height: 16px;
margin-top: -8px;
margin-left: -8px;
background-color: #fff;
border-radius: 16px;
transform: scale(0);
opacity: 0;
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
content: '';
}
}
.radio-checkeado {
border-color: var(--color-primario);
background-color: var(--color-primario);
&::after {
transform: scale(0.375);
opacity: 1;
}
}
.radio-etiqueta {
word-break: break-all;
padding: 0 8px;
}
}
.radio-deshabilitado {
color: rgba(0,0,0,0.25);
cursor: not-allowed;
.radio-mango {
background-color: rgba(0,0,0,0.04);
border-color: #d9d9d9;
&::after {
transform: scale(0.5);
background-color: rgba(0,0,0,0.25);
}
}
}
.radio-boton {
position: relative;
height: 32px;
padding-inline: 15px;
line-height: 30px;
background: #fff;
border: 1px solid #d9d9d9;
cursor: pointer;
transition: all 0.2s, box-shadow 0.2s;
&:first-child {
border-left: 1px solid #d9d9d9;
border-radius: 6px 0 0 6px;
}
&:last-child {
border-radius: 0 6px 6px 0;
}
&:not(:first-child):not(.radio-boton-unico)::before {
position: absolute;
top: -1px;
left: -1px;
content: '';
width: 1px;
height: 100%;
background-color: #d9d9d9;
}
&:not(.radio-boton-deshabilitado):hover {
color: var(--color-primario);
}
}
.radio-boton-unico {
border-left: 1px solid #d9d9d9;
border-radius: 6px;
}
.radio-boton-checkeado:not(.radio-boton-deshabilitado) {
z-index: 1;
color: var(--color-primario);
background-color: #fff;
border-color: var(--color-primario);
&::before {
background-color: var(--color-primario);
}
}
.radio-boton-deshabilitado {
color: rgba(0,0,0,0.25);
background-color: rgba(0,0,0,0.04);
border-color: #d9d9d9;
cursor: not-allowed;
&.radio-boton-checkeado {
background-color: rgba(0,0,0,0.15);
}
}
.radio-boton-solido.radio-boton-checkeado:not(.radio-boton-deshabilitado) {
color: #fff;
background-color: var(--color-primario);
border-color: var(--color-primario);
&:hover { color: #fff; }
}
.radio-boton-pequeno {
height: 24px;
padding-inline: 7px;
line-height: 22px;
&:first-child { border-radius: 4px 0 0 4px; }
&:last-child { border-radius: 0 4px 4px 0; }
&.radio-boton-unico { border-radius: 4px; }
}
.radio-boton-grande {
height: 40px;
font-size: 16px;
line-height: 38px;
&:first-child { border-radius: 8px 0 0 8px; }
&:last-child { border-radius: 0 8px 8px 0; }
&.radio-boton-unico { border-radius: 8px; }
}
.radio-onda {
position: absolute;
pointer-events: none;
top: 0; right: 0; bottom: 0; left: 0;
animation-iteration-count: 1;
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0,0,0.2,1);
border-radius: inherit;
}
.onda-activa {
animation-name: ondaExpandir, ondaOpacidad;
@keyframes ondaExpandir {
from { box-shadow: 0 0 0.5px 0 var(--color-primario); }
to { box-shadow: 0 0 0.5px 5px var(--color-primario); }
}
@keyframes ondaOpacidad {
from { opacity: 0.6; }
to { opacity: 0; }
}
}
</style>
Ejemplo de uso en un componente
A continuación se muestra cómo importar y utilizar el componente Radio en una página. Se incluyen ejemplos con opciones, estilos de botón, deshabilitación y personalización de espaciado.
<script setup lang="ts">
import Radio from './Radio.vue'
import { ref, watchEffect } from 'vue'
import type { Opcion } from '../types'
const opciones = ref<Opcion[]>([
{ etiqueta: 'Madrid', valor: 1 },
{ etiqueta: 'París', valor: 2 },
{ etiqueta: 'Berlín', valor: 3 },
{ etiqueta: 'Roma', valor: 4 },
{ etiqueta: 'Londres', valor: 5 },
{ etiqueta: 'Ámsterdam', valor: 6 }
])
const opcionesDeshabilitadas = ref<Opcion[]>([
{ etiqueta: 'Madrid', valor: 1 },
{ etiqueta: 'París', valor: 2, deshabilitado: true },
{ etiqueta: 'Berlín', valor: 3 },
{ etiqueta: 'Roma', valor: 4 },
{ etiqueta: 'Londres', valor: 5 },
{ etiqueta: 'Ámsterdam', valor: 6 }
])
const opcionesTamanio = [
{ etiqueta: 'small', valor: 'small' },
{ etiqueta: 'middle', valor: 'middle' },
{ etiqueta: 'large', valor: 'large' }
]
const chequear = ref<boolean>(false)
const valorSeleccion = ref<string | number | boolean>(2)
const tamBoton = ref<'small' | 'middle' | 'large'>('middle')
watchEffect(() => console.log('checked:', chequear.value))
watchEffect(() => console.log('value:', valorSeleccion.value))
const espaciadoH = ref(16)
const espaciadoV = ref(8)
function onCambio(val: string | number | boolean) {
console.log('cambio', val)
}
</script>
<template>
<div>
<h1>Radio - Ejemplos</h1>
<h2>Uso básico</h2>
<Radio v-model:modelValueCheck="chequear" @cambio="onCambio">Opción única</Radio>
<h2>Lista de opciones</h2>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" @cambio="onCambio" />
<h2>Estilo botón</h2>
<Radio v-model:modelValueCheck="chequear" boton>Boton Radio</Radio>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" boton />
<h2>Botón relleno</h2>
<Radio v-model:modelValueCheck="chequear" boton estiloBoton="solid">Boton Sólido</Radio>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" boton estiloBoton="solid" />
<h2>Deshabilitado</h2>
<Radio v-model:modelValueCheck="chequear" deshabilitado>Radio deshab</Radio>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" deshabilitado />
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" boton deshabilitado />
<h2>Opciones deshabilitadas</h2>
<Radio :opciones="opcionesDeshabilitadas" v-model:modelValue="valorSeleccion" />
<Radio :opciones="opcionesDeshabilitadas" v-model:modelValue="valorSeleccion" boton />
<Radio :opciones="opcionesDeshabilitadas" v-model:modelValue="valorSeleccion" boton estiloBoton="solid" />
<h2>Disposición vertical</h2>
<Radio vertical :opciones="opciones" v-model:modelValue="valorSeleccion" />
<h2>Personalizar etiqueta</h2>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion">
<template #default="{ option, label, index }">
<span v-if="index === 1" style="color: #ff6900">{{ label }}</span>
<span v-if="index === 3" style="color: #1677ff">{{ option.etiqueta }}</span>
</template>
</Radio>
<h2>Espaciado personalizado</h2>
<div>
<div>Espaciado horizontal: <input type="range" v-model.number="espaciadoH" min="0" max="40" /> {{ espaciadoH }}px</div>
<div>Espaciado vertical: <input type="range" v-model.number="espaciadoV" min="0" max="40" /> {{ espaciadoV }}px</div>
<Radio :espaciado="[espaciadoH, espaciadoV]" :opciones="opciones" v-model:modelValue="valorSeleccion" />
</div>
<h2>Tamaño del botón</h2>
<Radio :opciones="opcionesTamanio" v-model:modelValue="tamBoton" />
<Radio v-model:modelValueCheck="chequear" boton :tamanioBoton="tamBoton">Botón {{ tamBoton }}</Radio>
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" boton :tamanioBoton="tamBoton" />
<Radio :opciones="opciones" v-model:modelValue="valorSeleccion" boton estiloBoton="solid" :tamanioBoton="tamBoton" />
</div>
</template>
Este componente ofrece fleixbilidad para adaptarse a diferentes diseños, ya sea con radios clásicos o con apariencia de botones, con opciones de tamaño, espaciado y estilos visuales.