Mad\Http\MadResponse é o builder fluente de operações parciais
de UI — cada método empilha uma op (['op' => '...', ...])
no array $ops; send() serializa tudo como JSON e
encerra a execução. Toda action que retorna MadResponse tem
suas ops mescladas com as geradas automaticamente pelo
diff de auto-bind (ver Auto-bind).
void vs MadResponse
// void — so muda estado. O handler faz o diff e gera val/bind/reload_* sozinho.
public function onTipoChange(string $tipo): void
{
$this->form->set('descricao', TipoProduto::find($tipo)->descricao);
}
// MadResponse — quando voce precisa de UX explicito (toast, fechar, navegar)
public function onSave(): MadResponse
{
$produto = Produto::findOrNew($this->registroId);
$this->form->save($produto);
return (new MadResponse())
->toast('Salvo!', 'success')
->closeDrawer();
}
// Os dois mundos se mesclam: ops explicitos + auto-bind do diff, na mesma resposta
public function onAprovar(): MadResponse
{
$this->form->set('status', 'aprovado'); // auto: val op
return MadToast::success('Pedido aprovado!'); // explicito: toast op
}
| Situação | Retorno |
Só mudar estado (props, form->set()) | void — handler gera ops automaticamente |
| Toast / abrir-fechar drawer-modal / redirect | MadResponse |
| Substituir HTML de uma região específica | MadResponse->html() |
| Atualizar uma row/card sem reload da listagem | MadResponse->manageRow() / ->manageCard() |
| Erro de validação num campo específico | MadResponse->fieldError() |
| Mesclar estado + ops explícitas | MadResponse (mescladas com o auto-bind automaticamente) |
Manipulação de DOM
| Op | Faz |
html($seletor, $html) | Substitui o innerHTML do elemento |
val($seletor, $valor, $onlyEmpty = false) | Define o value de input/select/textarea |
attr($seletor, $atributo, $valor) | Define um atributo HTML qualquer |
addClass($seletor, $classe) / removeClass(...) | Adiciona/remove classe CSS |
show($seletor) / hide($seletor) | Mostra/esconde (display) |
return (new MadResponse())
->html('#preview', $htmlGerado) // substitui innerHTML
->val('[name="cep"]', '01310-100') // value de input/select/textarea
->attr('#link', 'href', $url) // qualquer atributo HTML
->addClass('#linha-1', 'mad-highlight')
->removeClass('#linha-1', 'mad-pending')
->show('#painel-avancado')
->hide('#painel-basico');
Erros de campo
| Op | Faz |
fieldError($campo, $mensagem) | Mostra erro em [data-field-error="campo"] + marca #campo como inválido |
dfFieldError($detailName, $campo, $mensagem) | Idem, mas escopado a um <mad-detail-form> (não conflita com campo de mesmo nome no master) |
clearFieldError($campo) | Remove o erro |
public function onSave(): MadResponse
{
if (empty($this->form->get('email'))) {
return (new MadResponse())->fieldError('email', 'E-mail é obrigatório.');
}
// ...
}
// Limpa quando o usuário corrige (normalmente disparado por mad:change)
public function onEmailChange(string $value): void
{
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
(new MadResponse())->clearFieldError('email')->send();
}
}
Recarregar options (combo, radio, checkbox-group...)
Mesma família dos ops reload_* que o auto-bind gera sozinho ao
detectar mudança num campo registrado — aqui você os dispara explicitamente,
sem depender do registry.
| Op | Componente alvo |
reloadCombo($name, $items, $selected?, $placeholder?) | <select> nativo / mad-select-field / mad-dbcombo-field |
reloadRadio($name, $items, $selected?) | mad-radio-field / mad-dbradio-field |
reloadCheckboxGroup($name, $items, $selected = []) | mad-checkbox-group-field / mad-dbcheckbox-group-field |
reloadMultiEntry($name, $items, $selected = []) | mad-multi-entry-field |
reloadSortList($name, $items, $selected = []) | mad-sort-list-field |
reloadChecklist($name, $items, $selected = []) | mad-checklist-field / mad-dbchecklist-field |
reloadCompletion($var, $items) | Autocomplete genérico (data-mad-autocomplete="var") |
addComboOption($name, $value, $label, $select = true, $scope = '') | Insere uma option nova (op combo_add_option) sem recarregar a lista — padrão quick-add. $scope é o mad-id do componente onde procurar o campo antes de cair na busca global (evita acertar um <select> homônimo de outra tela aberta); o JS também casa com name="X[]" |
// Combo nativo / mad-dbcombo-field — basta o name, sem renderizar HTML
$response->reloadCombo('cidade_id', $cidades, selected: '42', placeholder: 'Selecione...');
// Radio
$response->reloadRadio('tipo', ['A' => 'Ativo', 'I' => 'Inativo'], selected: 'A');
// Checkbox group / multi-entry / sort-list — mesmo shape (value => label)
$response->reloadCheckboxGroup('perms', ['read' => 'Ler', 'write' => 'Escrever'], selected: ['read']);
$response->reloadMultiEntry('tags', ['pro' => 'Pro', 'free' => 'Free'], selected: ['pro']);
$response->reloadSortList('ordem', ['id' => 'Código', 'nome' => 'Nome'], selected: ['nome', 'id']);
// Checklist — rows associativas (mesmo shape do model->toArray())
$response->reloadChecklist('grupos', $rows, selected: ['1', '3']);
// Autocomplete genérico (data-mad-autocomplete="methods")
$response->reloadCompletion('methods', ['onSave', 'onEdit', 'onDelete']);
// INSERIR uma option nova sem recarregar a lista inteira (padrão quick-add:
// abriu um cadastro em modal, salvou, quer o registro novo já selecionado).
// Em combo de seleção MULTIPLA a option é SOMADA à seleção atual.
$response->addComboOption('cidade_id', (string) $cidade->id, $cidade->nome, select: true, scope: $this->_getId());
Feedback ao usuário
| Op | Faz |
toast($mensagem, $type = 'info', $title = '', $position = 'top-right') | Notificação não-bloqueante |
alert($mensagem, $type = 'info', $title = '') | Diálogo de alerta bloqueante |
// type: info | success | warning | danger
$response->toast('Salvo com sucesso!', 'success');
$response->toast('Limite quase atingido.', 'warning', title: 'Atenção');
$response->alert('Esta ação é irreversível.', 'danger', 'Confirmação');
Navegação e overlays
| Op | Faz |
redirect($classeOuMetodo, $params = [], $delayMs = 0) | Full-page navigation para uma classe MAD (resolve rota amigável) |
redirectUrl($url, $delayMs = 0) | Full-page navigation para uma URL pronta |
openDrawer($nome) / openModal($nome) | Abre overlay por nome (CustomEvent) |
closeDrawer($nome = '') / closeModal($nome = '') | Fecha por nome, ou o overlay do componente atual se omitido |
MadResponse::open($classe, $params = [], $method = 'show') | Estático — abre um MadComponent detectando DRAWER/MODAL (overlay) vs INTERNAL (navega) |
// Navegação full-page para outra classe MAD (resolve rota amigável /app/slug)
return (new MadResponse())->redirect('PedidoForm@onEdit', ['id' => $pedido->id]);
// URL já pronta (ex.: destino pós-login)
return (new MadResponse())->redirectUrl('/app/dashboard');
// Abrir modal/drawer por nome (CustomEvent — qualquer um na página pode escutar)
return (new MadResponse())->openDrawer('filtros-avancados');
return (new MadResponse())->openModal('confirmar-exclusao');
// Fechar o overlay do PRÓPRIO componente (modal/drawer que o renderizou)
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
// Abrir um MadComponent detectando o wrapper sozinho (DRAWER/MODAL -> overlay; INTERNAL -> navega)
return MadResponse::open('DocProdutoForm', ['id' => $id]);
return MadResponse::open('DocProdutoForm', ['id' => $id], 'onEdit');
Render parcial sem full reload
Cobertura detalhada em Render parcial — aqui, a referência rápida das ops:
| Op | Faz |
manageRow($id, $gridClass) | Insere/atualiza uma row do MadDataGrid (com highlight) |
removeRow($id, $gridClass = '') | Remove uma row do MadDataGrid |
manageCard($id, $kanbanClass) | Insere/atualiza um card do MadKanban |
removeCard($id) | Remove um card do MadKanban |
reload($seletor, 'Classe@metodo', $params = []) | Substitui innerHTML pelo retorno (echo) de um método estático |
energize($nome, $params = []) | Recarrega um MadTransporter nomeado (round-trip client) |
teleport($seletor, $classe, $metodo = '', $params = []) | Renderiza um MadComponent inteiro direto no servidor, sem round-trip |
bind($prop, $valor) | Atualiza todo [data-mad-bind="prop"] (spans de @madBind) e o Alpine state de qualquer [x-data] do wrapper que tenha essa prop (scopes criados por @madWire) — escopado ao wrapper, nunca global |
// Atualiza (ou insere) UMA row do MadDataGrid sem reload da listagem inteira.
// Já busca o HTML server-side via MadDataGrid::renderSingleRow().
public function onSave(): MadResponse
{
$produto = Produto::findOrNew($this->registroId);
$this->form->save($produto);
return (new MadResponse())
->toast('Salvo!', 'success')
->closeDrawer()
->manageRow($produto->id, ProdutoListagem::class);
}
// Remover uma row (ex: após exclusão)
return (new MadResponse())->removeRow($id, ProdutoListagem::class);
// Mesmo padrão para o MadKanban — cards em vez de rows
return (new MadResponse())
->toast('Movido!', 'success')
->manageCard($tarefa->id, MeuKanban::class);
return (new MadResponse())->removeCard($tarefa->id);
// Recarrega um trecho chamando um método estático PHP (Classe@metodo) —
// o retorno (echo) vira o novo innerHTML do seletor alvo.
return (new MadResponse())->reload('#painel-itens', 'PedidoForm@renderItens', ['pedido_id' => $id]);
// energize() — round-trip client: dispara Mad.energize(name), que faz um
// POST e troca o conteudo do MadTransporter "name" pelo HTML renderizado.
return (new MadResponse())->energize('painel-financeiro', ['periodo' => 'mes']);
// teleport() — SEM round-trip: monta, faz mount()+method() e injeta o HTML
// direto na resposta atual (síncrono, dentro do mesmo request).
return (new MadResponse())
->teleport('#painel-detalhe', DocumentDetail::class, 'onShow', ['id' => 42]);
// Atualiza um span @madBind('prop') sem disparar o diff completo do auto-bind.
// A MESMA op tambem patcha o Alpine state de qualquer scope @madWire(['total'])
// do wrapper — mad-text/mad-if/mad-show reagem sozinhos, sem novo POST.
// O valor e coagido ao tipo que ja esta no state Alpine (number/boolean/string).
public function recalcular(): MadResponse
{
$this->total = number_format($this->calcularTotal(), 2, ',', '.');
return (new MadResponse())->bind('total', $this->total);
}
| Op | Faz |
dfAdd($detailName, $row, $editIndex = -1) | Insere/atualiza row num <mad-detail-form> |
dfDelete($detailName, $index) | Remove row de um <mad-detail-form> |
treeAddNode($tree, $nodeData) | Adiciona nó na tree-view |
treeRemoveNode($tree, $id) | Remove nó (com animação) |
treeUpdateNode($tree, $id, $data) | Atualiza label/icon/count de um nó existente |
treeSetActive($tree, $id) | Seleciona um nó, expandindo ancestrais se preciso |
// Hooks de before-add/before-delete de um <mad-detail-form> — confirmam (ou
// rejeitam) a row no servidor antes de refletir na listagem inline.
public function onBeforeAddItem(array $row): MadResponse
{
$produto = Produto::findOrFail($row['produto_id']);
$row['descricao'] = $produto->nome;
$row['valor'] = $produto->preco;
return (new MadResponse())->dfAdd('itens', $row);
}
public function onBeforeDeleteItem(int $index): MadResponse
{
return (new MadResponse())->dfDelete('itens', $index);
}
return (new MadResponse())
->treeAddNode('pastas', ['id' => $f->id, 'parent_id' => $f->parent_id, 'name' => $f->nome])
->toast('Pasta criada!', 'success');
(new MadResponse())->treeRemoveNode('pastas', $id)->send();
(new MadResponse())->treeUpdateNode('pastas', $id, ['name' => 'Novo nome', 'count' => 12])->send();
(new MadResponse())->treeSetActive('pastas', $id)->send();
Ops de Gantt
MadResponse também expõe ganttPatchTask(),
ganttPatchTasks(), ganttUpsertTask(),
ganttRemoveTask(), ganttUpsertRecord() e
ganttPatchRecord() para patchar barras do
MadGantt sem reload — específicas o bastante para
ficarem fora desta página; ver o docblock de cada método em
Mad\Http\MadResponse.
Diversos
| Método | Faz |
script($js) | Executa JavaScript arbitrário — último recurso, prefira um op dedicado |
refetchCalendar($calendarId) | Manda um MadFullCalendar rebuscar seus eventos (açúcar sobre script(): chama refetchEvents() no scope Alpine do container) |
dumpModal($dumps, $meta = []) | Overlay de debug client-side (op dump_modal) — normalmente você não chama na mão: send() injeta sozinho o que mad_dump_modal()/mdm() acumulou no request |
merge(MadResponse $other) | Funde as ops de outra instância na atual |
getOps() | Retorna o array de ops sem enviar (usado internamente pelo handler) |
send() | never — serializa como JSON e encerra a execução |
emit() | Imprime um <script> inline que aplica as ops — para contextos fora do wire (ex: pós-logout) |
MadResponse::ok($msg) / ::err($msg) | Atalho estático: toast + send() num passo só |
// Último recurso — JS arbitrário. Prefira os ops dedicados sempre que existir um.
return (new MadResponse())->script("window.dispatchEvent(new CustomEvent('relatorio:pronto'))");
public function onSave(): MadResponse
{
$response = new MadResponse();
if ($this->precisaAvisoEstoque()) {
$response->toast('Estoque baixo neste produto.', 'warning');
}
return $response->merge($this->salvarESincronizar());
}
// Atalhos estáticos — toast + send() num só passo, terminam a execução (never)
MadResponse::ok('Operação realizada com sucesso!');
MadResponse::err('Não foi possível processar.');
Encadeando tudo
Todo método retorna static — encadeie livremente na ordem que fizer sentido para a UX:
public function onSave(): MadResponse
{
try {
$this->form->validate(Produto::rules());
$produto = Produto::findOrNew($this->registroId);
$this->form->save($produto);
return (new MadResponse())
->toast('Produto salvo!', 'success')
->closeDrawer()
->manageRow($produto->id, ProdutoListagem::class);
} catch (\Mad\Form\MadValidationException $e) {
return $e->asInline(); // erros inline nos campos + toast de aviso
}
}
Próximos passos