Validação
Modelo::rules(), MadValidationException, asModal/asToast/asInline/asDetailForm e o canal de erro por slot [data-field-error].
O MAD valida formulários no servidor via Modelo::rules() + $this->form->validate(),
que delega diretamente ao illuminate/validation nativo do Laravel —
todas as regras padrão do Laravel funcionam (required, email,
unique, exists, numeric, confirmed,
image, regex, etc.). Erros lançam
MadValidationException, que sabe se apresentar como modal, toast, inline ou
response customizada — sem você montar HTML de erro à mão.
Declarando regras no model
Convenção: método estático rules($id = null) no model — recebe o ID do
registro em edição (null em criação), útil para regras unique
que precisam ignorar o próprio registro:
// app/Models/Produto.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Produto extends Model
{
protected $connection = 'business';
protected $table = 'produto';
public static function rules($id = null): array
{
return [
'nome' => 'required|max:200',
'cod_barras' => 'max:50|unique:produto,cod_barras,' . ($id ?? 'NULL') . ',id',
'valor' => 'required|numeric|min:0',
'email' => 'nullable|email',
'categoria_id' => 'required|exists:categoria,id',
'foto' => 'nullable|image|max:5120', // 5MB
];
}
}
Mad\Form\MadValidator registra um Illuminate\Translation\Translator
próprio com as mensagens padrão do illuminate/validation já traduzidas para
pt-BR — você não precisa publicar nem traduzir o pacote de validação do Laravel
para ter mensagens em português.
Validando no controller
public function onSave(): MadResponse
{
try {
$this->form->validate(Produto::rules($this->registroId));
// Se chegou aqui, todos os campos passaram — getData() já foi cacheado.
$produto = Produto::findOrNew($this->registroId);
$this->form->save($produto);
$this->registroId = (int) $produto->id;
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
} catch (MadValidationException $e) {
return $e->asModal(); // dialog com a lista de erros
} catch (\Throwable $e) {
return MadMessage::error('Erro', $e->getMessage());
}
}
Apresentando erros
MadValidationException tem quatro métodos de apresentação — todos retornam MadResponse:
asModal() — recomendado
catch (MadValidationException $e) {
return $e->asModal();
}
// Dialog estilizado (MadDialog, no client) com a lista de erros — bom
// default, não depende de o campo estar visível na tela no momento do erro.
asToast()
catch (MadValidationException $e) {
return $e->asToast();
}
// Toast com só a primeira mensagem de erro — sucinto, bom para forms curtos.
asInline()
catch (MadValidationException $e) {
return $e->asInline();
}
// Marca cada campo com erro inline (data-field-error) + toast de aviso
// genérico ("Corrija os erros antes de continuar."). Toast é opcional:
// $e->asInline(toast: ''); // omite o toast, só os campos
asDetailForm($name)
// Dentro do processRow() de um detail-form (ver Detail-form):
catch (MadValidationException $e) {
return $e->asDetailForm('itens'); // erro scoped à linha do detail "itens"
}
| Método | Quando usar |
|---|---|
asModal($title = 'Erros encontrados') | Default seguro — lista de erros num dialog, não depende do campo estar visível |
asToast($prefix = '') | Forms curtos — mostra só a primeira mensagem |
asInline($toast = '...') | Forms com vários campos visíveis simultaneamente — marca cada um com erro |
asDetailForm($dfName) | Erro de validação dentro do processamento de uma linha de detail-form |
Como o erro inline chega ao campo
asInline() (e, na mão, MadResponse::fieldError($campo, $msg))
escreve num slot de erro — o elemento
[data-field-error="<campo>"] que todo componente de campo renderiza.
Três detalhes desse canal existem porque cada um deles já engoliu mensagens de erro
em silêncio:
| Situação | O que o framework faz |
|---|---|
| Campo sem erro | O slot sempre está no DOM (renderizado vazio ou com o
hint). Slot criado só quando há erro seria slot inexistente na
hora de aplicar a op — a mensagem sumiria. |
Campo de mesmo name num detail-form |
Os seletores do master excluem o sub-form
([data-field-error="x"]:not([data-df-fields] *)) — sem isso o
querySelector pegaria o primeiro match do documento e o detail
roubaria o erro do master. Erro dentro do detail é
dfFieldError($dfName, $campo, $msg) / asDetailForm(). |
Detail-form em mode="drawer" |
O sub-form é teleportado para o body pelo drawer e
sai de dentro de [data-mad-df-name]. O JS tem fallback global por
[data-df-fields][data-df-name="<nome>"] para achar o container
teleportado. |
Campo dentro de <mad-tab-panel> inativa |
O slot existe mas está com display:none (x-show) — o
usuário só veria o toast. Mad._revealFieldError()
ativa a aba que contém o slot. A primeira aba com erro vence
(lock de 100ms no root das tabs impede que erros seguintes da mesma resposta
fiquem trocando de aba). |
Se você escreve um componente de campo customizado, renderize o
[data-field-error="{{ $name }}"] incondicionalmente (vazio quando
não há erro). Sem o elemento no DOM, asInline() aplica a op no vácuo e
o usuário só vê o toast genérico. O <mad-field-list> não tem slot
por célula — erro de linha se apresenta por asModal()/asToast().
Mensagens customizadas
O 2º argumento de validate() aceita mensagens customizadas no formato
'campo.regra' => 'mensagem' (convenção dot do illuminate/validation):
public static function rules($id = null): array
{
return [
'nome' => 'required|max:200',
'email' => 'nullable|email',
];
}
// Mensagens customizadas: 2º arg de validate() — chave 'campo.regra' (dot,
// convenção illuminate/validation), valor é a mensagem
public function onSave(): MadResponse
{
try {
$this->form->validate(Produto::rules($this->registroId), [
'nome.required' => 'Por favor, informe o nome.',
'nome.max' => 'Nome muito longo (máx. 200 caracteres).',
'email.email' => 'Email inválido.',
]);
// ...
} catch (MadValidationException $e) {
return $e->asModal();
}
}
Atalho — label inline na chave da regra
A chave da regra aceita 'campo|Label' — o label é extraído
automaticamente e usado para compor a mensagem padrão (ex: "O campo Nome do
Produto é obrigatório" em vez de "O campo nome..."):
public static function rules($id = null): array
{
return [
// chave 'campo|Label' — o label é extraído e usado na mensagem padrão
'nome|Nome do Produto' => 'required|string|max:255',
'email' => 'required|email',
];
}
Sem nenhum $attrs explícito, o validate() também tenta usar
os labels coletados automaticamente do HTML pelo MadWire (atributo label
de cada componente de campo) — o atalho 'campo|Label' só é necessário
quando você quer um texto diferente do label visível na tela.
Validação cross-field
Para regras que envolvem múltiplos campos ao mesmo tempo, lance MadValidationException manualmente depois da validação básica:
public function onSave(): MadResponse
{
try {
$data = $this->form->getData();
// Validação básica de cada campo
$this->form->validate(Produto::rules($this->registroId));
// Validação cross-field — não cabe numa regra por campo isolada
if ($data->valor_promo > 0 && $data->valor_promo >= $data->valor) {
throw new MadValidationException([
'valor_promo' => 'Valor promocional deve ser menor que o valor base.',
]);
}
$produto = Produto::findOrNew($this->registroId);
$this->form->save($produto);
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
} catch (MadValidationException $e) {
return $e->asModal();
}
}
Uso direto do MadValidator (fora de MadForm)
Em services, jobs ou qualquer contexto sem um MadForm à mão,
Mad\Form\MadValidator::validate() funciona isoladamente:
// Uso direto do MadValidator, fora do contexto de MadForm (ex: num service,
// num job, numa API). Retorna array de erros — [] se válido.
$errors = \Mad\Form\MadValidator::validate(
['nome' => 'Jo', 'email' => 'invalido'],
['nome' => 'required|min:3', 'email' => 'required|email']
);
// ['nome' => 'O campo nome deve ter no mínimo 3 caracteres.', 'email' => 'O campo email deve ser um endereço de e-mail válido.']