Field-list
Linhas dinâmicas inline: colunas, on-change, upload por linha e props condicionais por-row.
Field-list é uma tabela editável inline dentro de um formulário —
adicionar/remover linhas dinamicamente, com validação por linha, autocomplete e
callbacks ao mudar o valor de uma célula. Diferente do
detail-form
(que persiste em tabela 1:N automaticamente quando declara model +
foreign-key), o field-list por padrão trabalha com um array bruto — só
persiste sozinho em tabela relacionada se você também declarar model +
foreign-key na tag.
Estrutura básica
<mad-field-list name="acoes" addable removable>
<mad-field-list-column field="act_name" label="Nome" type="text" required />
<mad-field-list-column field="act_method" label="Método" type="text" required />
<mad-field-list-column field="act_icon" label="Ícone" type="text" />
</mad-field-list>
O componente gera um grid CSS com header, linhas dinâmicas e botão de adicionar/remover por linha — totalmente client-side (Alpine), sem round-trip ao servidor para inserir/remover linha.
Tipos de coluna
| type | Descrição |
|---|---|
text | Input texto padrão |
number | Input numérico nativo |
numeric | Decimal formatado pt-BR (decimals, min, max) |
money | Reverse-mask monetário (decimals, prefix) |
discount / combo_input | Valor + seletor de tipo companion (ex: % / R$) |
date | Data dd/mm/aaaa com datepicker |
select | Combo com options fixas (:items ou :options) |
dbcombo | Combo via Eloquent (model + display, com where/depends-on opcionais) |
checkbox / toggle | Booleano |
radio | Grupo de radio inline (:options) |
textarea | Multilinha |
file / multifile | Upload por linha — ver Upload de arquivos |
files | Upload multi-arquivo por linha em tabela neto (modal de galeria) |
hidden | Não renderiza input visível — útil com compute |
Combo dentro do field-list
<mad-field-list name="itens" addable removable>
<mad-field-list-column field="produto_id" label="Produto"
type="dbcombo" model="Produto" display="nome" required />
<mad-field-list-column field="quantidade" label="Qtd"
type="number" required />
<mad-field-list-column field="valor" label="Valor"
type="money" required sum />
</mad-field-list>
on-change — chamar PHP ao mudar célula
Quando o valor de uma coluna muda, dispara um método PHP (atributo
on-change sem $, sem espaços) que pode atualizar outras
colunas da mesma linha via form->set('campo[]', $valor):
<mad-field-list name="itens" addable removable>
<mad-field-list-column field="produto_id" label="Produto"
type="dbcombo" model="Produto" display="nome"
on-change="onChangeProduto" />
<mad-field-list-column field="quantidade" label="Qtd"
type="number" on-change="onChangeQuantidade" />
<mad-field-list-column field="valor" label="Valor unitário"
type="money" />
<mad-field-list-column field="total" label="Total"
type="money" attrs="readonly" />
</mad-field-list>
public function onChangeProduto($value): void
{
if (empty($value)) return;
$produto = Produto::find($value);
if (!$produto) return;
// Atualiza apenas a row de origem (auto-bind via convenção campo[])
$this->form->set('valor[]', $produto->preco);
$this->form->set('total[]', $produto->preco);
}
public function onChangeQuantidade($value): void
{
// Recalcula o total da row atual a partir do valor unitário já na row
$row = $_POST['mad_row'] ?? [];
$valor = (float) ($row['valor'] ?? 0);
$total = $valor * (float) $value;
$this->form->set('total[]', $total);
}
[] para a row atual
Dentro de on-change, form->set('campo[]', $v) patcha
apenas a row de origem do evento — sem afetar outras linhas. Funciona porque o
JS captura row = el.closest('.mad-fl-row') antes do POST. Ver
set() e setItems().
Coluna dependente (cascata)
depends-on faz uma coluna dbcombo recarregar suas options
automaticamente quando o valor de outra coluna da mesma linha muda — sem precisar
escrever um on-change manual:
<mad-field-list name="itens" addable removable>
<mad-field-list-column field="categoria_id" label="Categoria"
type="dbcombo" model="Categoria" display="nome" />
<mad-field-list-column field="produto_id" label="Produto"
type="dbcombo" model="Produto" display="nome"
depends-on="categoria_id" depends-column="categoria_id" />
</mad-field-list>
Coluna computada
compute recalcula o valor da coluna no client (Alpine x-effect)
a partir de uma expressão JS que recebe a row inteira — útil combinado com
type="hidden" para totais por linha sem round-trip:
<mad-field-list name="itens" addable removable>
<mad-field-list-column field="quantidade" label="Qtd" type="number" />
<mad-field-list-column field="valor" label="Valor" type="money" />
<mad-field-list-column field="total" label="Total" type="hidden"
compute="madCalcLineTotal(row)" />
</mad-field-list>
Upload por linha
Colunas file, multifile e files gravam arquivo
por linha no mesmo $form->save() do mestre — nenhum código de
arquivo no controller. As props ficam na própria coluna:
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
storage | string | '' (vira disk no save) | disk ou db |
folder | string | 'uploads' | Pasta no disco quando storage="disk" |
file-name | string | 'prefix' | Estratégia do nome no disco: prefix, unique, original, record |
name-column | string | '' | Coluna do filho para o nome original |
accept | string | '' | Filtro de tipos: .pdf,.jpg,image/* |
max-size | int (KB) | 0 | Tamanho máximo, 0 = sem limite |
foreign-key | string | '' | Só type="files": FK do NETO apontando para a linha do field-list |
path-column | string | '' | Só type="files": coluna do neto com o path (disk) ou o BLOB base64 (db) |
type="files" abre um modal de galeria e grava uma linha de tabela
neto por arquivo (reusando model, storage,
folder, file-name, name-column da coluna). Os discos
e a estratégia de nome são os mesmos do formulário — ver
Upload de arquivos.
Props condicionais por-row
Quatro atributos ligam o estado de uma célula ao conteúdo da própria linha, sem round-trip: a condição vira um bind Alpine avaliado por row.
| Atributo | Tipo | Default | Descrição |
|---|---|---|---|
disabled-when | string | '' | Desabilita o controle da célula (:disabled) |
readonly-when | string | '' | readonly condicional — só nos controles de texto |
required-when | string | '' | required condicional |
visible-when | string | '' | x-show na célula inteira |
<mad-field-list name="campos" addable removable>
<mad-field-list-column field="field_type" label="Tipo" type="select"
:options="['text' => 'Texto', 'select' => 'Combo']" />
<mad-field-list-column field="options" label="Opções" type="text"
disabled-when="{field_type} != 'select'"
required-when="{field_type} == 'select'" />
<mad-field-list-column field="codigo" label="Código" type="text"
readonly-when="{field_type} == 'select'" />
<mad-field-list-column field="mascara" label="Máscara" type="text"
visible-when="{field_type} == 'text'" />
</mad-field-list>
{campo} e o que visible-when NÃO faz
{campo} é compilado para row['campo']
(FieldListColumn::compileCondition); uma expressão sem
{token} passa crua como JS Alpine, com row no escopo.
Atenção: visible-when usa x-show — é só CSS, a célula
continua sendo submetida. Controle desabilitado não posta, então
o framework emite um hidden companion nos controles com name
próprio para o POST nunca desalinhar as colunas.
Ler dados do field-list
public function onSave(): MadResponse
{
// Lê todas as linhas — descobre os campos automaticamente via MadFormRegistry
$rows = $this->form->getFieldList('itens');
// [
// ['produto_id' => '5', 'quantidade' => '2', 'valor' => '10.00', 'total' => '20.00'],
// ['produto_id' => '7', 'quantidade' => '1', 'valor' => '50.00', 'total' => '50.00'],
// ]
foreach ($rows as $row) {
// ... persistir cada linha conforme a regra do app ...
}
return MadToast::success('Salvo!');
}
Persistência manual via MadFieldListTrait
Quando o field-list mapeia para uma tabela 1:N mas você não quer
declarar model/foreign-key direto na tag (ex: precisa de
filtro extra na query de load), use MadFieldListTrait +
loadDetailRows()/saveDetailItems():
class PedidoForm extends MadComponent
{
use \Mad\Form\MadFieldListTrait;
public function onEdit(int $id): void
{
$pedido = PedidoVenda::findOrFail($id);
$this->form->fill($pedido);
// Carrega itens da tabela filha e já injeta no field-list "itens"
$this->loadDetailRows('PedidoItem', 'pedido_id', $id, fieldListName: 'itens');
}
public function onSave(): MadResponse
{
try {
$pedido = PedidoVenda::findOrNew($this->registroId);
$this->form->save($pedido);
// Salva itens via smart-sync (update/insert + delete dos ausentes)
$this->saveDetailItems(
'PedidoItem',
'pedido_id',
$pedido->id,
$this->form->getFieldList('itens')
);
return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
} catch (\Throwable $e) {
return MadMessage::error('Erro', $e->getMessage());
}
}
}
Para tabela 1:N sem filtro extra, prefira
<mad-detail-form>,
que faz auto-load e auto-save só com model + foreign-key
na tag — sem precisar do trait. Use field-list quando precisar de uma tabela
inline mais leve, OU quando os dados não vão para uma tabela relacionada (ex:
array serializado num campo do model pai).
Atributos da tag <mad-field-list>
| Atributo | Tipo | Default | Descrição |
|---|---|---|---|
name | string | — | Nome do field-list (usado em form->getFieldList()) |
addable | bool | false | Mostra botão de adicionar linha |
removable | bool | false | Mostra botão de remover por linha |
sortable | bool | false | Permite reordenar por drag-and-drop |
max-rows | int | 0 | Linhas máximas (0 = ilimitado) |
add-label | string | 'Adicionar linha' | Texto do botão de adicionar |
model | string | '' | Classe Eloquent do filho — habilita auto-load/auto-save 1:N |
foreign-key | string | '' | Coluna FK no model filho |
database | string | '' | Conexão do banco (default: conexão do model) |
label / hint / error | string | '' | Label do campo, texto de ajuda, mensagem de erro |
on-add / on-remove / on-totalize | string | '' | Hooks JS opcionais para eventos do field-list |
Autocomplete numa coluna
Mesma convenção do form principal: prop pública com array de sugestões + data-mad-autocomplete:
class MeuForm extends MadComponent
{
public array $methods = []; // nome bate com data-mad-autocomplete="methods"
public function mount(): void
{
$this->methods = ['onSave', 'onCancel', 'onPrint', 'onExport'];
}
}
<mad-field-list name="acoes" addable removable>
<mad-field-list-column field="method" label="Método" type="text"
attrs='data-mad-autocomplete="methods" autocomplete="off"' />
</mad-field-list>
Field-list vs. detail-form
| Preciso de... | Usar |
|---|---|
| Tabela editável inline, poucas colunas, cabe no form | <mad-field-list> |
| Form longo por item (drawer/modal próprio) | <mad-detail-form mode="drawer"> |
| Persistência automática em tabela 1:N | <mad-detail-form> ou <mad-field-list>, ambos com model + foreign-key |
| Array num campo serializado do model pai (sem tabela filha) | <mad-field-list> sem model/foreign-key + leitura manual via getFieldList() |
| Smart-sync (update existentes, insert novos, delete sumidos) | Ambos fazem — é o comportamento padrão quando model/foreign-key estão declarados |