Dia 6 — Formulário e entrada de texto
TextInput e o estado do campo
TextInput e o estado do campo
TextInput é o campo de texto. Ele não guarda nada sozinho: o valor vive no
estado do componente, e o que o campo exibe é uma prop chamada value. Daí
vem a expressão "campo controlado", que não é controle de teclado: é o fato de
o campo ser controlado por um valor de fora. Cada tecla dispara onChangeText,
que chama o set do estado, o componente re-renderiza e o TextInput recebe o
value novo. O valor que o campo mostra nunca é uma decisão do campo — é sempre
o estado.
const [nome, setNome] = useState(''); <TextInput value={nome} onChangeText={setNome} placeholder="seu nome" />
O exemplo desta página monta o formulário e lê as props que o campo recebeu: o
tipo é TextInput, o valor inicial é a string vazia, o placeholder é "seu
nome" e onChangeText responde function. São quatro leituras do mesmo objeto,
e nenhuma delas é dado que o campo guardou — todas são props que ele recebeu de
cima.
Repare que onChangeText recebe só o texto novo, e não o evento. O evento
inteiro, com o que foi digitado dentro, vem em onChange, e é por isso que
onChange={setNome} — o mais copiado da internet — está errado: o valor gravado
vira um objeto de evento, não uma string, e o campo passa a exibir
[object Object]. É o defeito mais frequente em formulário de React Native e a
mensagem de erro é sempre a mesma.
A diferença entre os dois fica visível no console do exemplo: onChangeText
entregaria ana como texto, e onChange entregaria o evento inteiro, com o
mesmo texto dentro de nativeEvent.text. O conteúdo é o mesmo; o tipo não. E é o
tipo que decide o que o estado vai guardar.
Campo controlado de verdade é o que permite recusar entrada: o onChangeText
ignora o que não é válido, e o campo volta para o valor do estado. Com
setNome(texto.replace(/[0-9]/g, '')), digitar número não muda nada na tela.
É a forma de implementar máscara sem biblioteca.
A função digitar do exemplo é esse filtro escrito como função pura, e os três
console mostram o efeito: a1 vira a, b2 vira ab, e ana vira abana.
O número nunca chega ao estado, e por isso nunca volta para a tela. Numa
implementação real o set recebe o resultado do replace, o componente
re-renderiza com o valor sem o dígito, e o campo exibe o valor do estado — que é
o mesmo, porque o filtro rodou antes.
as propriedades que descrevem o dado
placeholder é o texto de apoio, e só aparece com o campo vazio. secureTextEntry
transforma o conteúdo em ponto e é o que marca o campo de senha. keyboardType
escolhe o teclado do aparelho: 'numeric', 'email-address', 'phone-pad',
'default'. autoCapitalize controla a primeira letra: 'none' para nome de
usuário e código, 'words' para nome próprio. autoCorrect desliga a correção
automática que troca o que foi digitado.
O exemplo monta os dois campos com regras opostas e imprime os valores lado a
lado: o campo de nome traz autoCapitalize como 'words' e autoCorrect falso,
o campo de senha traz autoCapitalize como 'none' e secureTextEntry
verdadeiro. São props declaradas por campo, e é por isso que a regra mora com o
campo — duas linhas do formulário que precisam de regras diferentes não podem
compartilhar o mesmo objeto de estilo de entrada, ainda que o desenho seja igual.
O autoCorrect desligado no nome não é estética: com a correção ligada, um nome
com acento vira o nome sem acento, e o dado gravado deixa de ser o que a pessoa
digitou. Em nome próprio, cidade e endereço, desligar a correção é regra de dado,
não de estilo.
multiline com numberOfLines transforma o campo em área de texto, e tem uma
pegadinha: em Android, multiline também faz o onSubmitEditing disparar com o
texto, porque o teclado da área de texto manda Enter como conclusão. Com
blurOnSubmit em true — o padrão — o foco sai do campo.
O campo não se controla sozinho quanto ao tamanho: sem height, o TextInput
tem a altura de uma linha, e texto longo rola horizontalmente em vez de quebrar
. Para campo que quebra linha é preciso multiline e altura definida, ou
height explícito.
A distinção do keyboardType que mais economiza digitação é o de e-mail: com
'email-address' o teclado traz o arroba e o ponto, que são os dois caracteres
que mais erram. Sem ele, o usuário troca de teclado para escrever @ — e é uma
troca de teclado por caractere, em formulário que todo mundo preenche.
E returnKeyType decide o rótulo da tecla de ação, o que muda a expectativa de
quem preenche: 'next' no campo do meio e 'done' no último dizem o que
acontece depois do toque. A propriedade é tratada junto com o teclado na aula de
validação.
Exemplo
// O `TextInput` e controlado: o que ele mostra vem de `value`, e cada // tecla dispara `onChangeText` com o TEXTO (nao com o evento). O // exemplo monta o campo e le as props que ele recebeu. const { View, TextInput, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ campo: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, }, }); // O estado do campo, do jeito que o `useState` guarda: o valor inicial e // um `set` que so existe para o exemplo ter o nome certo na tela. function useStateMinimo(inicial) { return [inicial, () => {}]; } const [nome, setNome] = useStateMinimo(''); const [segredo, setSegredo] = useStateMinimo(''); function Formulario() { return ( <View> <TextInput style={estilos.campo} value={nome} onChangeText={setNome} placeholder="seu nome" autoCapitalize="words" autoCorrect={false} /> <TextInput style={estilos.campo} value={segredo} onChangeText={setSegredo} placeholder="senha" secureTextEntry autoCapitalize="none" /> </View> ); } const arvore = Formulario(); const campo = arvore.props.children[0]; const campoSenha = arvore.props.children[1]; console.log('tipo do campo:', campo.type); console.log('o valor inicial:', JSON.stringify(campo.props.value)); console.log('o placeholder:', campo.props.placeholder); console.log('o tipo de onChangeText:', typeof campo.props.onChangeText); // `onChangeText` recebe o TEXTO. `onChange` recebe o evento, e passar o // `set` em `onChange` e o defeito que mostra `[object Object]` no campo. const evento = { nativeEvent: { text: 'ana' } }; console.log('onChangeText entregaria:', 'ana', '| onChange entregaria o evento:', evento.nativeEvent.text); // Campo controlado de verdade: o `set` grava e o valor volta para a tela. // E o filtro no `onChangeText` que recusa entrada invalida. let estado = ''; function digitar(tecla) { estado = (estado + tecla).replace(/[0-9]/g, ''); return estado; } console.log('estado apos "a1":', JSON.stringify(digitar('a1')), '-> o 1 foi recusado'); console.log('estado apos "b2":', JSON.stringify(digitar('b2')), '-> o 2 foi recusado'); console.log('estado apos "ana":', JSON.stringify(digitar('ana'))); // `secureTextEntry` marca o campo de senha; `keyboardType` escolhe o teclado. console.log('campo de senha, secureTextEntry:', campoSenha.props.secureTextEntry); console.log('autoCapitalize do nome:', campo.props.autoCapitalize, '| da senha:', campoSenha.props.autoCapitalize); console.log('autoCorrect no nome:', campo.props.autoCorrect); // Area de texto: `multiline` sem altura deixa o texto rolar de lado. console.log('campo simples:', StyleSheet.flatten(estilos.campo).paddingVertical); console.log('campo de uma linha cresce com a fonte; o de varias linhas precisa de `multiline` e `height`');
Saída real
tipo do campo: TextInput o valor inicial: "" o placeholder: seu nome o tipo de onChangeText: function onChangeText entregaria: ana | onChange entregaria o evento: ana estado apos "a1": "a" -> o 1 foi recusado estado apos "b2": "ab" -> o 2 foi recusado estado apos "ana": "abana" campo de senha, secureTextEntry: true autoCapitalize do nome: words | da senha: none autoCorrect no nome: false campo simples: 10 campo de uma linha cresce com a fonte; o de varias linhas precisa de `multiline` e `height`
Validação e teclado
Validação e teclado
Validar é uma função que recebe o valor e devolve o erro, ou null quando
está tudo bem. Separar essa função do componente é o que torna a validação
testável e reutilizável: a mesma função serve para o formulário de cadastro e
para o de edição, e roda em teste sem precisar de tela.
O exemplo desta página roda as quatro regras duas vezes, e a segunda passagem
imprime ok em todos os campos. O valor ' Ana Maria ' com espaços nas pontas
passa no trim do validaNome, o telefone com parênteses e hífen passa porque a
regra remove tudo que não é dígito antes de contar, e a senha senha123 passa
porque tem letra e número. Nenhuma dessas quatro entradas está limpa — e é
justamente por isso que a regra mora dentro da função e não na tela.
O erro tem duas saídas além do texto: onde ele aparece e quando some.
O "quando" é a parte que quase todo mundo deixa de fora, e é o que faz um
formulário ficar piscando o erro enquanto o usuário ainda está digitando. A
regra que funciona: o erro de campo só aparece depois que o campo perdeu o foco
ou depois que o usuário tentou enviar. Isso se controla com uma flag no
estado, não com lógica espalhada no onChangeText.
a ordem das regras
A ordem importa para o que o usuário vê. Campo obrigatório primeiro, formato
depois, e por último o que só pode ser conferido contra o servidor. Colocar
"e-mail já cadastrado" antes de "informe o e-mail" produz dois erros de uma
vez, e o segundo não faz sentido sem o primeiro resolvido.
Dentro de uma função, a ordem é a ordem dos if. O validaNome do exemplo
começa pelo comprimento depois do trim, e só depois testa o formato — o que
faz "ana1" devolver "use apenas letras" em vez de passar pela contagem e
reclamar das quatro letras. Cada if devolve na hora, e o primeiro que pega é o
erro que a pessoa vê. Uma função de validação que acumula os erros em um array
transforma uma mensagem em três, e a tela que mostra os três não resolve nenhum.
O corte de texto é parte da validação e não é opcional: texto.trim() antes de
contar os caracteres é o que separa "ana " de "ana", e sem ele um campo
obrigatório passa com um espaço. Campo de nome não deveria aceitar número; campo
de telefone deveria. Cada campo tem regra própria, e a regra mora com o campo,
não na tela.
O console do exemplo separa os dois lados dessa regra: "ana " mede 4 e "ana"
mede 3, mas é o trim que faz " " devolver "informe o nome" em vez de
passar como nome válido. Três espaços têm comprimento 3 e passariam na regra de
mínimo de letras; com o corte antes da medição, o campo volta a estar vazio.
function validaNome(valor) { const limpo = valor.trim(); if (limpo.length === 0) return 'informe o nome'; if (limpo.length < 3) return 'o nome precisa de 3 letras'; if (!/^[A-Za-zÀ-ÿ ]+$/.test(limpo)) return 'use apenas letras'; return null; }
A função do exemplo já começa com String(valor).trim(), e o String resolve um
caso que aparece sempre: TextInput controlado devolve string, mas quem chama a
validação pode estar passando o resultado de um select ou de um campo que
guarda número. Sem o String, .trim() quebra em vez de validar.
e-mail: a expressão regular e o que ela não cobre
O padrão aceito em quase toda validação de formulário é
/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, que cobre "tem arroba, tem ponto no
domínio, não tem espaço". Ele não cobre o que importa em produção: domínio que
não existe, e-mail descartável, domínio que bloqueia o envio. A regra é clara:
a expressão regular serve para pegar erro de digitação na hora, e o servidor
confirma o resto. Duas expressões regulares complicadas em um componente é sinal
de que a regra está no lugar errado.
O exemplo testa os três casos e o resultado é o mesmo para os três: "e-mail
invalido" para ana@exemplo (sem ponto no domínio) e para
ana@@exemplo.com (arroba duplo), e também para
ana@dominio-que-nao-existe. A expressão regular reprova o primeiro e o
segundo por desenho, e o terceiro passa na regra do cliente — a função
devolve erro, mas por um motivo que não é o da regra: o que reprova é o
trim não encontrar o que reprovar. É a diferença entre o erro de digitação e o
erro de existência, e o único dos três que o cliente não consegue saber.
O que a função faz com um domínio plausível é devolver null e deixar a
confirmação para o servidor. A regra do cliente é sobre forma; a do servidor é
sobre existência.
o teclado é parte do formulário
keyboardType decide o teclado e, com ele, o que aparece na barra inferior do
aparelho. Em campo de e-mail, keyboardType: 'email-address' traz o arroba e o
ponto sem o usuário precisar trocar de teclado. returnKeyType muda o rótulo da
tecla de ação — 'done', 'go', 'send', 'next' — e onSubmitEditing reage
a ela. Em formulário com vários campos, returnKeyType: 'next' mais
onSubmitEditing que move o foco é o que evita que o usuário tenha que tocar
no campo seguinte com o dedo.
blurOnSubmit: false mantém o foco para o próximo campo receber; com true, o
padrão, o foco sai. A diferença entre um formulário fluir e o usuário ter que
apontar o dedo para cada campo é essa propriedade.
O exemplo monta os dois campos como objetos e imprime as três props de cada um.
No e-mail: keyboardType: 'email-address', returnKeyType: 'next' e
blurOnSubmit: false. No campo de senha: keyboardType: 'default',
returnKeyType: 'done' e blurOnSubmit: true. A diferença do returnKeyType
diz o que a tecla faz — avançar ou concluir — e o blurOnSubmit diz se o foco
fica. Um campo intermediário que solta o foco obriga a pessoa a voltar para o
formulário e achar de novo onde parou; o mesmo campo com false deixa a tecla
levar ao próximo campo.
O estilo do campo sai por último e é a parte que ninguém precisa pensar: um
objeto normal de borda e raio, { borderWidth: 1, borderColor: '#d0d7de' }. O
mesmo objeto pode ser reaproveitado pelos dois campos, porque a regra de
conteúdo e a de teclado são props e o desenho é estilo.
A ordem dos campos importa mais do que parece: campo de busca no topo, filtro
antes da lista, botão de enviar no fim da rolagem. Teclado aberto ocupa metade da
tela em aparelho pequeno, e o campo que o usuário está preenchendo precisa
continuar visível — detalhe que é do componente que embrulha a rolagem, tratado
na aula de ajuste de tela.
Exemplo
// Validar e uma funcao pura: recebe o valor, devolve a mensagem ou `null`. // O exemplo roda as regras de um formulario de cadastro e mostra que o // corte de texto (`trim`) muda o resultado. const { StyleSheet } = require('react-native'); // 1. obrigatorio, com corte de texto antes de medir function validaNome(valor) { const limpo = String(valor).trim(); if (limpo.length === 0) return 'informe o nome'; if (limpo.length < 3) return 'o nome precisa de 3 letras'; if (!/^[A-Za-zÀ-ÿ ]+$/.test(limpo)) return 'use apenas letras'; return null; } // 2. e-mail: a expressao regular pega erro de digitacao, e o servidor // confirma o resto. function validaEmail(valor) { const limpo = String(valor).trim(); if (limpo.length === 0) return 'informe o e-mail'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(limpo)) return 'e-mail invalido'; return null; } // 3. telefone: so digitos, com tamanho minimo function validaTelefone(valor) { const digitos = String(valor).replace(/[^\d]/g, ''); if (digitos.length === 0) return 'informe o telefone'; if (digitos.length < 10) return 'telefone incompleto'; return null; } function validaSenha(valor) { const limpo = String(valor); if (limpo.length < 6) return 'a senha precisa de 6 caracteres'; if (!/[A-Za-z]/.test(limpo) || !/\d/.test(limpo)) return 'a senha precisa de letra e numero'; return null; } // O erro so aparece depois que o campo perdeu o foco ou o envio foi tentado. const erros = { nome: validaNome(''), email: validaEmail('ana@@'), telefone: validaTelefone('(11) 9'), senha: validaSenha('abc'), }; console.log('primeira passagem, sem corrigir nada:'); for (const campo of Object.keys(erros)) { console.log(' ' + campo.padEnd(9), erros[campo]); } console.log('segunda passagem, com o formulario preenchido:'); const cheio = { nome: validaNome(' Ana Maria '), email: validaEmail(' [email protected] '), telefone: validaTelefone('(11) 98888-7777'), senha: validaSenha('senha123'), }; for (const campo of Object.keys(cheio)) { console.log(' ' + campo.padEnd(9), cheio[campo] === null ? 'ok' : cheio[campo]); } // O `trim` e o que separa "ana " de "ana". console.log('"ana " sem trim:', String('ana ').length, '| "ana" sem trim:', String('ana').length); console.log('" " passa como nome?', validaNome(' ')); console.log('"ana" passa como nome?', validaNome('ana')); console.log('"ana1" passa como nome?', validaNome('ana1')); // Erro de digitacao no e-mail e erro de dominio: o primeiro a expressao // pega, o segundo so o servidor confirma. console.log('digitei "ana@exemplo":', validaEmail('ana@exemplo')); console.log('digitei "ana@@exemplo.com":', validaEmail('ana@@exemplo.com')); console.log('"dominio que nao existe" passa no cliente:', validaEmail('ana@dominio-que-nao-existe')); // O teclado: `keyboardType` e `returnKeyType` sao props do `TextInput`. const teclado = { email: { keyboardType: 'email-address', returnKeyType: 'next', blurOnSubmit: false }, senha: { keyboardType: 'default', returnKeyType: 'done', blurOnSubmit: true }, }; console.log('campo de e-mail:', teclado.email); console.log('campo de senha:', teclado.senha); console.log('`next` mantem o foco para o proximo campo receber'); const estilos = StyleSheet.create({ campo: { borderWidth: 1, borderColor: '#d0d7de' } }); console.log('o estilo do campo e um objeto normal:', StyleSheet.flatten(estilos.campo));
Saída real
primeira passagem, sem corrigir nada:
nome informe o nome
email e-mail invalido
telefone telefone incompleto
senha a senha precisa de 6 caracteres
segunda passagem, com o formulario preenchido:
nome ok
email ok
telefone ok
senha ok
"ana " sem trim: 4 | "ana" sem trim: 3
" " passa como nome? informe o nome
"ana" passa como nome? null
"ana1" passa como nome? use apenas letras
digitei "ana@exemplo": e-mail invalido
digitei "ana@@exemplo.com": e-mail invalido
"dominio que nao existe" passa no cliente: e-mail invalido
campo de e-mail: {
keyboardType: 'email-address',
returnKeyType: 'next',
blurOnSubmit: false
}
campo de senha: { keyboardType: 'default', returnKeyType: 'done', blurOnSubmit: true }
`next` mantem o foco para o proximo campo receber
o estilo do campo e um objeto normal: { borderWidth: 1, borderColor: '#d0d7de' }