mad-field-list
Tabela editável inline dentro de form.
Linhas dinâmicas inline para mestre-detalhe simples (cada linha edita seus
campos direto na tabela, sem abrir drawer/modal). Auto-load + auto-save
quando declarado com model + foreign-key — mesmo mecanismo do
<mad-detail-form>, só que com edição inline em vez de
sub-form.
Quando usar: itens de pedido, parcelas, anexos curtos, telefones, endereços — qualquer coleção 1:N com poucas colunas onde edição inline acelera a UX.
Quando NÃO usar: itens com muitas colunas, validação complexa por linha ou ações compostas →
<mad-detail-form>(abre um sub-form em drawer/modal/spa por linha).
Quick start
Blade
<mad-field-list name="itens" model="PedidoItem" foreign-key="pedido_id"
addable removable add-label="Adicionar item">
<mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
model="Produto" display="nome" required
on-change="onChangeProduto" />
<mad-field-list-column field="quantidade" label="Qtd." type="numeric"
width="90px" min="0.01" default="1" required />
<mad-field-list-column field="valor" label="Valor" type="money"
width="120px" prefix="R$" required />
<mad-field-list-column field="valor_total" label="Total" type="money"
readonly compute="{quantidade} * {valor}" sum />
</mad-field-list>
Controller (auto-load + auto-save)
use Illuminate\Support\Facades\DB;
public function onEdit(int $id): void
{
$pedido = Pedido::findOrFail($id); // leitura não precisa de transação
$this->registroId = (int) $pedido->id;
$this->form->fill($pedido); // auto-load das rows acontece aqui
}
public function onSave(): MadResponse
{
try {
$data = $this->form->getData();
$this->form->validate(Pedido::rules($this->registroId));
DB::transaction(function () {
$pedido = $this->registroId ? Pedido::findOrFail($this->registroId) : new Pedido();
$this->form->save($pedido); // auto-save das rows (smart-sync)
});
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
} catch (MadValidationException $e) {
return $e->asModal();
} catch (Throwable $e) {
return MadMessage::error('Erro', $e->getMessage());
}
}
Atributos do <mad-field-list>
| Attr | Tipo | Default | Descrição |
|---|---|---|---|
name |
string | — | Nome do field-list (obrigatório; acessado via $form->getFieldList($name)) |
model |
string | — | Classe do model Eloquent dos itens — habilita auto-load/save |
foreign-key |
string | — | Coluna FK no model do filho (ex: pedido_id) |
database |
string | MAIN_DATABASE | Conexão do model filho |
addable |
bool | false | Botão "+ adicionar linha" no rodapé |
removable |
bool | false | Botão "x" em cada linha |
sortable |
bool | false | Drag-and-drop pra reordenar linhas (handle à esquerda) |
add-label |
string | "Adicionar linha" | Texto do botão de adicionar |
max-rows |
int | 0 | Linhas máximas (0 = ilimitado; desabilita o botão de adicionar ao atingir) |
label |
string | '' | Label acima da tabela |
hint |
string | '' | Texto de ajuda |
error |
string | '' | Mensagem de erro |
class |
string | '' | Classes CSS extras |
on-add |
string | '' | Método PHP chamado (fire-and-forget) ao adicionar uma linha |
on-remove |
string | '' | Método PHP chamado ao remover uma linha |
on-totalize |
string | '' | Método PHP chamado quando os totais (sum/count) mudam (debounced) |
Não existem
min-rowsnemdefault-rowsno componente atual — linhas mínimas/obrigatórias são responsabilidade da validação ($this->form->validate(...)), não do field-list.
<mad-field-list-column> — coluna
Atributos comuns
| Attr | Descrição |
|---|---|
field |
Nome da coluna no model do filho (obrigatório) |
label |
Cabeçalho |
type |
ver tabela de tipos abaixo |
width |
Largura CSS fixa ('120px', '30%', '1fr') |
placeholder |
Placeholder do input |
default |
Valor pré-preenchido ao adicionar linha nova |
required / readonly / disabled |
Flags booleanas |
attrs |
Atributos HTML extras (ex: data-mad-autocomplete="...") |
attrssó é aplicado em colunas que renderizam um<input>simples —text/number/tel(e qualquertypenão tratado explicitamente). Emmoney,numeric,discount,combo_input,date,select,dbcombo,checkbox,toggle,radio,textarea,file,multifileefileso atributo é ignorado pelo template (cada um desses tipos renderiza markup próprio, sem passthrough deattrs).
Tipos (type=)
type= |
Quando usar | Props extras |
|---|---|---|
text (default) |
Texto simples | placeholder, default |
number |
Inteiro/decimal HTML5 nativo | min, max, step |
numeric |
Decimal pt-BR mascarado | decimals, min, max |
money |
Moeda | decimals, prefix (ex: R$), min, max |
date |
Data com datepicker | min, max |
discount |
Desconto: valor + select %/R$ |
type-field (default {field}_tipo), decimals |
combo_input |
Número + select inline (genérico) | options, type-field, decimals |
select |
Select estático | options (mapa 'A' => 'Ativo') |
dbcombo |
Select de banco (auto-load + cascata) | model, display, key-field, order-by, where, depends-on, depends-column, database (conexão do model de lookup, se diferente da padrão) |
hidden |
Campo escondido (não renderiza, só mapeia) | default |
email / tel |
Validação HTML5 | placeholder |
checkbox / toggle |
Booleano ('1'/'0') |
— |
radio |
1-de-N | options |
textarea |
Texto longo | placeholder |
file |
Upload único por linha | accept, max-size, storage, folder, file-name, name-column |
multifile |
Upload múltiplo por linha (mesma coluna) | idem file |
files |
Upload múltiplo via modal → tabela neto (1 linha por arquivo) | model, foreign-key, path-column, name-column, storage, folder |
<!-- dbcombo dentro de field-list -->
<mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
model="Produto" display="nome" required />
<!-- money com soma no rodapé -->
<mad-field-list-column field="subtotal" label="Subtotal" type="money"
decimals="2" sum />
<!-- select estático inline -->
<mad-field-list-column field="status" label="Status" type="select"
:options="['A' => 'Ativo', 'I' => 'Inativo']" />
Outros atributos (kebab-case → camelCase)
| Attr Blade | Método interno | Uso |
|---|---|---|
type-field |
typeField |
Coluna companheira de discount/combo_input (tipo %/R$) |
key-field |
keyField |
PK do model em dbcombo (default id) |
order-by |
orderBy |
Ordenação das options em dbcombo |
on-change |
onChange |
Método PHP (ou expressão Alpine) disparado no change da célula |
depends-on |
dependsOn |
Campo da própria linha que dispara reload em cascata |
depends-column |
dependsColumn |
Coluna do model filho a filtrar na cascata (default = depends-on) |
where |
— | Filtro estático nas options: 'ativo=1|tipo=P' ou 'preco:>:0' |
file-name |
fileName |
Estratégia do nome no disco: prefix | unique | original | record |
name-column |
nameColumn |
Coluna do model filho pro nome original do arquivo |
foreign-key |
foreignKey |
(só type="files") FK do neto apontando pra linha do field-list |
path-column |
pathColumn |
(só type="files") coluna do neto pro path (disk) ou BLOB (db) |
decimals, max-size |
int | Casas decimais / tamanho máximo (KB) |
min, max, step |
float | Limites numéricos |
sum / count |
flags | Totalizador no rodapé (soma ou contagem) |
compute |
string | Fórmula client-side (ver abaixo) |
items |
alias de options |
:items="[...]" funciona igual a :options="[...]" |
Estado condicional POR ROW (disabled-when e amigos) — 5.x
Quatro atributos ligam o estado do controle da célula ao conteúdo da própria
linha, sem round-trip no servidor (viram bind Alpine no x-for):
| Attr Blade | Método interno | Efeito |
|---|---|---|
disabled-when |
disabledWhen |
Desabilita o controle da célula |
readonly-when |
readonlyWhen |
readonly (só controles de texto) |
required-when |
requiredWhen |
required condicional |
visible-when |
visibleWhen |
x-show na célula inteira |
Sintaxe (FieldListColumn::compileCondition): {campo} vira
row['campo']. Se a expressão não tiver nenhum {token}, ela passa crua
como JS Alpine (row está no escopo) — compatibilidade com quem já escrevia
row['x'] === 'y'.
<mad-field-list-column field="field_type" label="Tipo" type="select"
:options="['text' => 'Texto', 'select' => 'Select']" />
<mad-field-list-column field="options" label="Opções" type="text"
disabled-when="{field_type} != 'select'" />
<mad-field-list-column field="desconto" label="Desconto" type="money"
readonly-when="{tipo} == 'F'" required-when="{qtd} > 0 && {ativo}" />
<mad-field-list-column field="obs" label="Obs" type="text"
visible-when="{tipo} == 'S'" />
Cuidado com
visible-when:x-showé só CSS — a célula continua no DOM e é SUBMETIDA. Para não gravar o valor, combine comdisabled-when(input desabilitado não entra no POST) ou limpe o campo noon-change.
compute — fórmula client-side
<mad-field-list-column field="valor_total" label="Total" type="money"
readonly compute="{quantidade} * {valor}" sum />
<mad-field-list-column field="liquido" label="Líquido" type="money"
readonly compute="discount({valor}, {desconto}, {tipo_desconto})" />
Funções disponíveis: operadores + - * /, round(expr, casas), max(...),
min(...), abs(...), floor(...), ceil(...), if(cond, then, else) e
discount(base, {valor_desc}, {tipo_desc}) (tipo='%' aplica percentual,
qualquer outro valor aplica como R$ fixo). {campo} resolve pro valor
atual da própria linha (reativo via Alpine x-effect).
sum — totalizador no rodapé
<mad-field-list-column field="valor_total" label="Total" type="money" sum />
Mostra soma total no footer (só aparece quando pelo menos uma coluna tem
sum ou count).
on-change — callback PHP por linha
<mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
model="Produto" display="nome"
on-change="onChangeProduto" />
public function onChangeProduto($value): void
{
$p = Produto::find($value);
if (!$p) return;
$this->form->set('valor[]', $p->preco); // op fl_val (escopo de row)
$this->form->set('unidade[]', $p->unidade);
\Mad\Form\FieldListColumn::loadOptions('familia_id', 'Familia', 'id', 'nome'); // op fl_combo
}
form->set('campo[]', ...) afeta apenas a row de origem do change event
(o JS captura row = el.closest('.mad-fl-row') antes do POST). Alternativa
fluente: \Mad\Form\FieldListColumn::setValue('campo', $valor) e
FieldListColumn::loadOptions($target, $model, $key, $display) — ambos
retornam MadResponse e podem ser encadeados com ->merge(...).
Ações por linha — <mad-field-list-action>
Botões customizados em cada linha (além do remover padrão), com dois modos:
action (mad:click server-side) ou navegação.
<mad-field-list name="itens" model="PedidoItem" foreign-key="pedido_id" removable>
<mad-field-list-column field="produto_id" label="Produto" type="dbcombo" model="Produto" display="nome" />
<mad-field-list-column field="quantidade" label="Qtd" type="numeric" />
<mad-field-list-action method="onDuplicar" icon="copy" title="Duplicar" />
<mad-field-list-action navigate="ProdutoDetalhe::show({produto_id})" icon="eye" title="Ver produto" />
<mad-field-list-action method="onZerar" icon="rotate-ccw" variant="danger" confirm="Zerar esta linha?" />
</mad-field-list>
| Attr | Descrição |
|---|---|
method |
Método mad:click no host (modo action) |
navigate |
Classe ou Classe::metodo({campo}) (modo navegação — placeholders resolvidos da própria row) |
icon |
Ícone Lucide |
label / title |
Texto do botão / tooltip |
variant |
danger | primary | success | warning | info | ghost |
confirm |
Mensagem de confirmação antes de disparar |
Auto-load + auto-save (smart-sync)
Quando model + foreign-key estão na tag, o field-list segue o mesmo
pipeline do <mad-detail-form> — ver
detalhamento completo em detail-form.md:
- Load (
$this->form->fill($mestre)): montaModel::query()->where('foreign_key', $mestre->id), ordena porid, normaliza as rows e popula$this->form->fields[$name]. - Save (
$this->form->save($mestre)): no_afterStore, cada row comidé carregada viafind($pkVal) ?? new $model()(gera UPDATE), rows semidgeram INSERT, e rows que sumiram do POST são deletadas (whereNotIn('id', $savedIds)). - Rows totalmente vazias (exceto chaves
__*) são ignoradas no save.
Hooks PHP
Registrar antes de form->save/form->fill (mesmo contrato do detail-form):
// onLoadDetail — transformar rows ao carregar
$this->form->onLoadDetail('itens', fn($item, $mestre) => [
'familia_id' => $item->produto_id ? ($item->produto->familia_id ?? '') : '',
]);
// onSaveDetail — transformar/validar ao salvar
$this->form->onSaveDetail('itens', function ($item, $mestre, $row): void {
$item->valor_total = (float) $item->valor * (float) $item->quantidade;
$mestre->valor_total += $item->valor_total;
});
Ordem de execução: setar colunas → setar FK → hook → save() da linha.
A versão atual do framework removeu o método
detailCriteria()(escopo extra de WHERE no load/delete). Tanto o auto-load/auto-save declarativo quanto oMadFieldListTraitescopam o detail só pelaforeign-key— se o filho compartilha tabela com outros tipos e você precisa de um filtro adicional, monte a query manualmente (ver escopo extra em detail-form.md).
Sem model no Blade — manual via Trait
use Mad\Form\MadFieldListTrait;
class MeuForm extends MadComponent
{
use MadFieldListTrait;
public function onEdit(int $id): void
{
$pedido = Pedido::findOrFail($id);
$this->registroId = (int) $pedido->id;
$this->form->fields['itens'] = $this->loadDetailRows('PedidoItem', 'pedido_id', $id);
}
public function onSave(): MadResponse
{
$data = $this->form->getData();
\Illuminate\Support\Facades\DB::transaction(function () use ($data) {
$pedido = $this->registroId ? Pedido::findOrFail($this->registroId) : new Pedido();
$pedido->save();
$this->saveDetailItems('PedidoItem', 'pedido_id', $pedido->id, $data->itens);
});
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
}
}
saveDetailItems() faz smart-sync (rows com id → update/insert + delete dos
ausentes) ou delete+insert quando as rows não têm coluna id. loadDetailRows()
carrega via Eloquent (Model::query()->where($fk, $parentId)) e normaliza as
rows prontas para o field-list.
Acesso a rows via PHP
$rows = $this->form->getFieldList('itens');
// [
// ['id' => 1, 'produto_id' => 5, 'quantidade' => 2, ...],
// ['id' => '', 'produto_id' => 7, 'quantidade' => 1, ...], // row nova
// ]
Útil para validações cruzadas, agregações, etc.
Gerar/manipular rows em PHP (FieldListColumn)
use Mad\Form\FieldListColumn;
// Gerar N linhas com valores pré-preenchidos (ex: 12 parcelas)
$this->parcelas = FieldListColumn::buildRows(12, function (int $i) use ($valorParcela, $dataBase) {
return [
'parcela' => $i + 1,
'valor' => round($valorParcela, 2),
'vencimento' => date('Y-m-d', strtotime("+{$i} months +1 month", strtotime($dataBase))),
];
});
$this->parcelas = FieldListColumn::clearRows(); // []
$this->parcelas = FieldListColumn::removeRow($this->parcelas, 2); // remove a 3ª linha
$this->parcelas = FieldListColumn::updateRow($this->parcelas, 0, ['valor' => 150.00]);
Pra empurrar rows pro client sem re-render completo, use a op parcial:
return FieldListColumn::setRows('parcelas', $this->parcelas);
Gotchas
form->set('campo[]', ...)só funciona em actions disparadas poron-changeda coluna (mad:fl-change) — não chamar em submit/click global, a opfl_valfica órfãcomputeé client-side — não confie para validação/storage; recompute noonSaveDetail- Não há canal de erro por-célula.
dfFieldError()é do<mad-detail-form>(escopo[data-df-fields]); o field-list não tem equivalente por linha. Para erro de item, useerrorno<mad-field-list>inteiro oufieldError()num campo master. E cuidado:fieldError('campo')usa o seletor[data-field-error="campo"]:not([data-df-fields] *)— se o field-list tiver coluna homônima a um campo master, o erro do master continua no lugar certo, mas não existe destino dentro da linha - Rows totalmente vazias são ignoradas no smart-sync — exceto chaves
__* - Não chame
saveDetailItems()manual quando o Blade temmodel+foreign-key— roda 2x (auto-save + manual) - FK na row é sempre forçada para
$mestre->{pk}— input com nome FK no field-list não tem efeito sumtotaliza apenas valores numéricos — strings/dates são ignoradas- Mudou colunas? Limpe
bootstrap/cache/blade-*/ cache de views Blade — o compilador gera PHP estático a partir das<mad-field-list-column>
Ver também: <mad-detail-form> para mestre-detalhe com
sub-form completo por linha.