feat: tipo cep e tipo select mais documentação do eli input (ainda incompleta)
This commit is contained in:
parent
6c84508996
commit
4fdc5a95ce
4 changed files with 215 additions and 6 deletions
|
|
@ -47,6 +47,28 @@
|
||||||
v-bind="attrs"
|
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 -->
|
<!-- RADIO -->
|
||||||
<v-radio-group
|
<v-radio-group
|
||||||
v-else-if="type === 'radio'"
|
v-else-if="type === 'radio'"
|
||||||
|
|
@ -80,6 +102,7 @@ import { defineComponent, ref, computed, PropType } from "vue";
|
||||||
import { formatarCpfCnpj } from "./utils/cpfCnpj";
|
import { formatarCpfCnpj } from "./utils/cpfCnpj";
|
||||||
import { formatTelefone } from "./utils/telefone";
|
import { formatTelefone } from "./utils/telefone";
|
||||||
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
|
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
|
||||||
|
import { formatarCep } from "./utils/cep";
|
||||||
|
|
||||||
type Option = {
|
type Option = {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -114,6 +137,8 @@ type InputType =
|
||||||
| "checkbox"
|
| "checkbox"
|
||||||
| "telefone"
|
| "telefone"
|
||||||
| "cpfCnpj"
|
| "cpfCnpj"
|
||||||
|
| "cep"
|
||||||
|
| "select"
|
||||||
| TipoNumerico;
|
| TipoNumerico;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -138,6 +163,8 @@ export default defineComponent({
|
||||||
color: { type: String, default: "primary" },
|
color: { type: String, default: "primary" },
|
||||||
row: Boolean,
|
row: Boolean,
|
||||||
showPasswordToggle: Boolean,
|
showPasswordToggle: Boolean,
|
||||||
|
multiple: Boolean,
|
||||||
|
chips: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ["update:modelValue", "change", "focus", "blur"],
|
emits: ["update:modelValue", "change", "focus", "blur"],
|
||||||
|
|
@ -166,6 +193,7 @@ export default defineComponent({
|
||||||
"numericoInteiro",
|
"numericoInteiro",
|
||||||
"numericoDecimal",
|
"numericoDecimal",
|
||||||
"numericoMoeda",
|
"numericoMoeda",
|
||||||
|
"cep",
|
||||||
].includes(props.type)
|
].includes(props.type)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -211,6 +239,10 @@ export default defineComponent({
|
||||||
case "cpfCnpj":
|
case "cpfCnpj":
|
||||||
resultado = formatarCpfCnpj(resultado);
|
resultado = formatarCpfCnpj(resultado);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "cep":
|
||||||
|
resultado = formatarCep(resultado);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.value = resultado;
|
target.value = resultado;
|
||||||
|
|
@ -222,6 +254,27 @@ export default defineComponent({
|
||||||
showPassword.value = !showPassword.value;
|
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 {
|
return {
|
||||||
attrs,
|
attrs,
|
||||||
value,
|
value,
|
||||||
|
|
@ -234,6 +287,9 @@ export default defineComponent({
|
||||||
onInput,
|
onInput,
|
||||||
onFocus: () => emit("focus"),
|
onFocus: () => emit("focus"),
|
||||||
onBlur: () => emit("blur"),
|
onBlur: () => emit("blur"),
|
||||||
|
computedItems,
|
||||||
|
optLabel,
|
||||||
|
optValue,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
9
src/componentes/EliInput/utils/cep.ts
Normal file
9
src/componentes/EliInput/utils/cep.ts
Normal 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");
|
||||||
|
}
|
||||||
|
|
@ -18,20 +18,44 @@
|
||||||
density="compact"
|
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="valor" type="numericoMoeda" label="Valor" />
|
||||||
|
|
||||||
<EliInput v-model="telefone" type="telefone" label="Telefone" />
|
<EliInput v-model="telefone" type="telefone" label="Telefone" />
|
||||||
|
|
||||||
<EliInput
|
<EliInput
|
||||||
v-model="documento"
|
v-model="cep"
|
||||||
type="cpfCnpj"
|
type="cep"
|
||||||
label="CPF / CNPJ"
|
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
|
<EliInput
|
||||||
v-model="email"
|
v-model="email"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
|
@ -108,6 +132,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const nome = ref("");
|
const nome = ref("");
|
||||||
|
const estado = ref([])
|
||||||
|
const cep = ref("");
|
||||||
const telefone = ref("");
|
const telefone = ref("");
|
||||||
const idade = ref("");
|
const idade = ref("");
|
||||||
const altura = ref("");
|
const altura = ref("");
|
||||||
|
|
@ -115,13 +141,14 @@ export default defineComponent({
|
||||||
const email = ref("");
|
const email = ref("");
|
||||||
const mensagem = ref("");
|
const mensagem = ref("");
|
||||||
const senha = ref("");
|
const senha = ref("");
|
||||||
const documento = ref("")
|
const documento = ref("");
|
||||||
const cor = ref(null);
|
const cor = ref(null);
|
||||||
const habilidades = ref<any[]>([]);
|
const habilidades = ref<any[]>([]);
|
||||||
return {
|
return {
|
||||||
nome,
|
nome,
|
||||||
email,
|
email,
|
||||||
documento,
|
documento,
|
||||||
|
estado,
|
||||||
telefone,
|
telefone,
|
||||||
mensagem,
|
mensagem,
|
||||||
senha,
|
senha,
|
||||||
|
|
@ -129,6 +156,7 @@ export default defineComponent({
|
||||||
habilidades,
|
habilidades,
|
||||||
idade,
|
idade,
|
||||||
altura,
|
altura,
|
||||||
|
cep,
|
||||||
valor,
|
valor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue