feat: tipo cep e tipo select mais documentação do eli input (ainda incompleta)

This commit is contained in:
andreLMpena 2025-12-23 15:41:00 -03:00
parent 6c84508996
commit 4fdc5a95ce
4 changed files with 215 additions and 6 deletions

View file

@ -47,6 +47,28 @@
v-bind="attrs"
/>
<!-- SELECT -->
<v-select
v-else-if="type === 'select'"
v-model="value"
:items="computedItems"
:label="label"
:placeholder="placeholder"
:multiple="multiple"
:chips="chips"
:clearable="clearable"
:disabled="disabled"
:density="density"
:variant="variant"
item-title="label"
item-value="value"
:error="error"
:error-messages="errorMessages"
v-bind="attrs"
@focus="onFocus"
@blur="onBlur"
/>
<!-- RADIO -->
<v-radio-group
v-else-if="type === 'radio'"
@ -80,6 +102,7 @@ import { defineComponent, ref, computed, PropType } from "vue";
import { formatarCpfCnpj } from "./utils/cpfCnpj";
import { formatTelefone } from "./utils/telefone";
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
import { formatarCep } from "./utils/cep";
type Option = {
label: string;
@ -114,6 +137,8 @@ type InputType =
| "checkbox"
| "telefone"
| "cpfCnpj"
| "cep"
| "select"
| TipoNumerico;
export default defineComponent({
@ -138,6 +163,8 @@ export default defineComponent({
color: { type: String, default: "primary" },
row: Boolean,
showPasswordToggle: Boolean,
multiple: Boolean,
chips: Boolean,
},
emits: ["update:modelValue", "change", "focus", "blur"],
@ -166,6 +193,7 @@ export default defineComponent({
"numericoInteiro",
"numericoDecimal",
"numericoMoeda",
"cep",
].includes(props.type)
);
@ -211,6 +239,10 @@ export default defineComponent({
case "cpfCnpj":
resultado = formatarCpfCnpj(resultado);
break;
case "cep":
resultado = formatarCep(resultado);
break;
}
target.value = resultado;
@ -222,6 +254,27 @@ export default defineComponent({
showPassword.value = !showPassword.value;
}
// --- Helpers para select / radio / checkbox (aceita objetos ou primitivos) ---
const computedItems = computed(() => {
// Normaliza options para [{ label, value, disabled? }]
return (props.options || []).map((o: any) =>
o && typeof o === "object" && ("label" in o || "value" in o)
? { label: o.label ?? String(o.value), value: o.value, disabled: o.disabled }
: { label: String(o), value: o }
);
});
function optLabel(opt: any) {
if (opt && typeof opt === "object") return opt.label ?? String(opt.value);
return String(opt);
}
function optValue(opt: any) {
if (opt && typeof opt === "object") return opt.value;
return opt;
}
return {
attrs,
value,
@ -234,6 +287,9 @@ export default defineComponent({
onInput,
onFocus: () => emit("focus"),
onBlur: () => emit("blur"),
computedItems,
optLabel,
optValue,
};
},
});

View file

@ -0,0 +1,116 @@
# EliInput
**Componente base de input do design system**
O EliInput unifica vários tipos de campo (v-text-field, v-textarea, v-select, v-radio-group, v-checkbox) em uma única API consistente. Ele encapsula comportamentos, máscaras e regras comuns (CPF/CNPJ, telefone, CEP, numéricos, formatação de moeda etc.) para manter coerência visual e de lógica em toda a aplicação.
> ⚠️ Nunca use os componentes Vuetify diretamente fora do design system para esses casos.
> Utilize sempre EliInput para garantir formatação, tipagem e repasse de atributos padronizados.
---
## Visão geral
EliInput foi projetado para:
* Centralizar formatação (máscaras) para tipos de entrada comuns (telefone, CPF/CNPJ, CEP, numéricos).
* Fornecer uma API única (type) que representa diferentes controles (text, textarea, select, radio, checkbox, etc.).
* Repassar atributos/props do pai para o componente Vuetify interno (v-bind="$attrs") mantendo inheritAttrs: false.
* Emitir eventos padronizados: update:modelValue, change, focus, blur.
---
## Principais decisões de implementação
* inheritAttrs: false — o componente controla explicitamente para onde os atributos são passados (v-bind="attrs" no elemento interno).
* Uso de um computed value que faz emit("update:modelValue", v) e emit("change", v) — assim qualquer v-model no pai funciona como esperado.
* Normalização de props.options para aceitar objetos { label, value, disabled } ou primitivos ('A', 1).
* Separação clara entre lógica de formatação (aplicada em onInput) e componentes que não devem ser formatados (ex.: v-select).
---
## Tipagem (TypeScript)
```ts
type Option = { label: string; value: any; disabled?: boolean };
type InputVariant = 'outlined' | 'filled' | 'plain' | 'solo' | 'solo-filled' | 'solo-inverted' | 'underlined';
type Density = 'default' | 'comfortable' | 'compact';
type TipoNumerico = 'numericoInteiro' | 'numericoDecimal' | 'numericoMoeda';
type InputType =
| 'text' | 'password' | 'email' | 'search' | 'url' | 'textarea'
| 'radio' | 'checkbox' | 'telefone' | 'cpfCnpj' | 'cep' | 'select'
| TipoNumerico;
```
---
## Props
| Prop | Tipo | Default | Descrição |
| ---------------- | --------------------------- | --------------- | ------------------------------------------------------ |
| `modelValue` | `string \| number \| any[]` | `""` | Valor controlado (use com `v-model`). |
| `type` | `InputType` | `"text"` | Tipo do controle (ver `InputType`). |
| `label` | `string` | `-` | Rótulo do campo. |
| `placeholder` | `string` | `-` | Texto exibido quando o campo está vazio. |
| `disabled` | `boolean` | `false` | Desabilita o campo. |
| `error` | `boolean` | `false` | Força estado visual de erro. |
| `errorMessages` | `string \| string[]` | `[]` | Mensagem ou lista de mensagens de erro. |
| `hint` | `string` | `-` | Texto de ajuda exibido abaixo do campo. |
| `persistentHint` | `boolean` | `false` | Mantém o hint sempre visível. |
| `variant` | `InputVariant` | `"outlined"` | Variante visual do Vuetify. |
| `density` | `Density` | `"comfortable"` | Densidade visual do campo. |
| `color` | `string` | `"primary"` | Cor do campo quando focado (ou `error`, se aplicável). |
| `clearable` | `boolean` | `false` | Permite limpar o valor do campo. |
## Notas sobre props
* options: aceita arrays como ['Frontend','Backend'] ou { label:'São Paulo', value:'SP' }. O componente normaliza para { label, value, disabled? }.
* type determina quais comportamentos internos são aplicados. Tipos numéricos e máscara (telefone, cpfCnpj, cep) passam por formatação em onInput.
* multiple/chips: úteis para type="select". O v-select interno recebe item-title="label" e item-value="value" para compatibilidade com objetos.
---
## Emissões (events)
* update:modelValue — padrão v-model.
* change — emitido sempre que o valor muda (alinha com update:modelValue).
* focus — quando o campo interno recebe foco.
* blur — quando perde o foco.
* Observação: como value é um computed com getter/setter que emite ambos, v-model e listeners de mudança no pai funcionarão normalmente.
---
## Repasso de atributos e listeners
* O componente define inheritAttrs: false e usa v-bind="attrs" nos componentes internos (v-text-field, v-select, etc.). Isso implica que:
* Atributos HTML (ex.: type, aria-label, class, style) passados para <EliInput> são aplicados ao componente Vuetify interno apropriado.
* Listeners (ex.: @click, @keydown) também fazem parte de $attrs e serão repassados para o componente interno — use o EliInput como se estivesse ouvindo eventos diretamente no input.
## Exemplo:
```vue
<EliInput type="text" v-model="nome" aria-label="Nome completo" @keydown.enter="enviar" />
```
---
## Comportamentos de formatação importantes
* numericoInteiro — remove tudo que não for dígito.
* numericoDecimal — mantém separador decimal (aplica formatarDecimal).
* numericoMoeda — formata para moeda conforme util (formatarMoeda).
* telefone — aplica máscara/format formatTelefone.
* cpfCnpj — aplica formatarCpfCnpj.
* cep — aplica formatarCep.
**Importante: a formatação ocorre no onInput (campos text-like). O v-select não passa por onInput — ele usa v-model="value" e o computed que emite o update. Se desejar formatação específica para itens do select (por exemplo, mostrar label formatado), trate nos options antes de passar.**
---
## Slot
* O componente não expõe slots customizados diretamente. Ele controla internamente o append para toggle de senha quando type === 'password' && showPasswordToggle (ícone de olho).
* Se você precisa de slots específicos do v-text-field/v-select, considere estender o componente ou criar uma variação que exponha os slots desejados.

View file

@ -0,0 +1,9 @@
import { somenteNumeros } from "./numerico";
export function formatarCep(v: string): string {
const d = somenteNumeros(v).slice(0, 8);
if (d.length <= 5) return d;
return d.replace(/^(\d{5})(\d{1,3})$/, "$1-$2");
}

View file

@ -18,20 +18,44 @@
density="compact"
/>
<EliInput v-model="idade" type="numericoInteiro" label="Idade" density="default" />
<EliInput
v-model="idade"
type="numericoInteiro"
label="Idade"
density="default"
/>
<EliInput v-model="altura" type="numericoDecimal" label="Altura" density="comfortable" />
<EliInput
v-model="altura"
type="numericoDecimal"
label="Altura"
density="comfortable"
/>
<EliInput v-model="valor" type="numericoMoeda" label="Valor" />
<EliInput v-model="telefone" type="telefone" label="Telefone" />
<EliInput
v-model="documento"
type="cpfCnpj"
label="CPF / CNPJ"
v-model="cep"
type="cep"
label="CEP"
placeholder="00000-000"
/>
<EliInput
type="select"
label="Estado"
:options="[
{ label: 'São Paulo', value: 'SP' },
{ label: 'Rio de Janeiro', value: 'RJ' }
]"
v-model="estado"
multiple
/>
<EliInput v-model="documento" type="cpfCnpj" label="CPF / CNPJ" />
<EliInput
v-model="email"
label="Email"
@ -108,6 +132,8 @@ export default defineComponent({
},
setup() {
const nome = ref("");
const estado = ref([])
const cep = ref("");
const telefone = ref("");
const idade = ref("");
const altura = ref("");
@ -115,13 +141,14 @@ export default defineComponent({
const email = ref("");
const mensagem = ref("");
const senha = ref("");
const documento = ref("")
const documento = ref("");
const cor = ref(null);
const habilidades = ref<any[]>([]);
return {
nome,
email,
documento,
estado,
telefone,
mensagem,
senha,
@ -129,6 +156,7 @@ export default defineComponent({
habilidades,
idade,
altura,
cep,
valor,
};
},