mad-db-blocks
Coleção 1:N vinculada a parent com CRUD inline.
Componente generico de lista-pivot com CRUD inline: chips, shares, links, attachments — qualquer coleção vinculada a um registro pai via pivot, com add/remove/update sem full re-render.
O visual de cada linha é delegado a um Blade partial (row-view) fornecido pelo consumidor. O form de adição é declarado como filho via <mad-db-blocks-form> e disparado por um botão "+ Adicionar" em popover/modal/drawer.
IMPORTANTE: o controller host DEVE adicionar use \Mad\Form\MadDbBlocksTrait;. Os metodos blockAdd, blockRemove, blockUpdate sao roteados contra o MadComponent host.
Props
| Prop | Tipo | Default | Descricao |
|---|---|---|---|
| name | string | '' | Identificador do bloco (obrigatorio, usado em IDs DOM) |
| mode | string | pivot |
pivot = coleção 1:N vinculada a um parent. flat = lista master independente (sem FK) |
| pivot-model | string | '' | Classe do model Eloquent (obrigatorio). Em mode=pivot aponta pra tabela pivot; em mode=flat aponta pro model master |
| database | string | MAIN_DATABASE | Conexao |
| foreign-key | string | '' | Coluna FK do registro pai na pivot. Obrigatorio em mode=pivot, ignorado em mode=flat |
| record-id | int | auto | ID do registro pai. Auto-resolve via MadRenderContext::getComponent()->registroId. Ignorado em mode=flat |
| row-view | string | '' | Path do Blade partial que renderiza UMA linha (obrigatorio) |
| order-by | string | 'id' | Coluna de ordenacao |
| order-dir | string | 'asc' | asc ou desc |
| layout | string | 'inline' | inline (chips, quebra linha) ou stack (linhas 100% largura) |
| add-mode | string | 'popover' | Onde o form inline abre: popover, modal, drawer |
| add-label | string | '+ Adicionar' | Texto do botao |
| add-icon | string | 'plus' | Aceito pelo componente, mas o botao padrao atual nao renderiza icone — so add-label aparece |
| add-variant | string | 'ghost' | Aceito pelo componente, mas o botao padrao atual nao aplica classe de variant — visual fixo (mad-db-blocks-add) |
| form-position | string | 'below' | Onde o form aparece no modo add-mode="inline": below ou above |
| submit-label | string | 'Salvar' | Texto do botao de submit do form do item |
| submit-icon | string | 'check' | Icone Lucide do botao de submit |
| hide-add-button | bool | false | Esconder botao de adicionar. Forcado a true quando add-mode="inline" |
| no-list | bool | false | Modo "so adicionar": nao pre-renderiza nem re-renderiza a lista (a exibicao dos itens fica com outro componente — ex.: a tree de pastas do GED) |
| editable | bool | false | Permite editar o item no lugar, reusando o mesmo form do adder ja preenchido |
| edit-fields | array | [] | Campos liberados para a edicao in-place |
| preset-vars | array | [] | Valores livres injetados no template do item — chegam ao row-view como $vars (usado pelos presets <mad-comments> / <mad-attachments>) |
| confirm-remove | bool|string | false | Confirmacao antes de remover. Opt-in: pelado usa a mensagem padrao (Remover este item?), com valor usa a sua. Chega ao row-view como $vars['confirmRemove']; os row-views do framework emitem data-mad-confirm, interceptado pelo wire antes da acao |
| empty-text | string | 'Nenhum item' | Texto quando lista vazia |
| on-add | string | '' | Metodo do host chamado dentro da transacao, antes do save() do novo pivot |
| on-remove | string | '' | Metodo do host chamado dentro da transacao, antes do delete() |
| on-update | string | '' | Metodo do host chamado dentro da transacao, antes do save() do update de campo |
| on-after-add | string | '' | Metodo do host chamado apos o commit de on-add — recebe ($pivot, MadResponse $resp), use pra acrescentar ops no response (ex: operacoes que precisam do commit ja aplicado, como manageRow) |
| on-after-remove | string | '' | Idem on-after-add, apos o commit da remocao |
| filters | array | [] | Filtros adicionais na query (sintaxe dbcombo) |
| class | string | '' | Classes CSS extras |
Upload por item (opcional)
Quando file-field e declarado, o blockAdd grava o(s) arquivo(s) desse campo
no disco de uploads e preenche path/metadados no proprio item.
Multi-arquivo = N itens.
| Prop | Tipo | Default | Descricao |
|---|---|---|---|
| file-field | string | '' | Nome do campo de arquivo no form do item — liga o upload |
| folder | string | 'uploads' | Pasta de destino no disco de uploads |
| path-column | string | '' | Coluna do item que recebe o path do arquivo |
| file-name | string | 'prefix' | Estrategia de nome do arquivo gravado |
| original-name-column | string | '' | Coluna que recebe o nome original enviado |
| size-column | string | '' | Coluna que recebe o tamanho em bytes |
| mime-column | string | '' | Coluna que recebe o mime-type |
| disk-column | string | '' | Coluna que recebe o disco usado |
Modos — pivot vs flat
O componente opera em dois modos distintos, controlados pelo atributo mode:
mode="pivot" (default) — coleção vinculada a um parent
Caso classico: tags/permissoes/links/attachments de um registro especifico. Ha sempre um record-id (do parent) e um foreign-key (coluna na pivot que referencia o parent). Ao adicionar um item, o componente automaticamente seta $pivot->{foreignKey} = recordId.
<mad-db-blocks name="tags"
pivot-model="GedDocumentTag"
foreign-key="document_id"
:record-id="$docId"
row-view="components.partials.tag-chip"
on-add="onTagCreate">
...
</mad-db-blocks>
mode="flat" — lista master independente
Para listas de registros master que nao tem parent: tags do sistema, categorias globais, etiquetas, qualquer collection de "master data" com CRUD inline. Nao ha foreign-key nem record-id — o componente opera diretamente sobre o model.
Nesse modo, pivot-model aponta pro model master (nao pra uma pivot real — o nome do atributo e mantido por compat retroativa).
<mad-db-blocks name="sidebar-tags"
mode="flat"
pivot-model="GedTag"
row-view="ged.partials.sidebar-tag-row"
on-add="onSidebarTagCreate"
on-remove="onSidebarTagRemove">
<mad-db-blocks-form>
<mad-input-field name="name" label="Nome" required />
<mad-color-field name="color" label="Cor" />
</mad-db-blocks-form>
</mad-db-blocks>
Hook on-add em flat mode recebe o proprio record master (nao uma pivot):
public function onSidebarTagCreate(GedTag $tag): void
{
$tag->created_by = session('userid');
if (empty($tag->color)) $tag->color = '#a78bfa';
}
Hook on-remove em flat mode deleta o record master. Se ha pivots que o referenciam, limpe-as no hook:
public function onSidebarTagRemove(GedTag $tag): void
{
GedDocumentTag::where('tag_id', '=', (int) $tag->id)->delete();
}
Quando usar cada modo
| Cenario | Usar |
|---|---|
| Tags/labels de um registro (doc, pedido, projeto) | mode="pivot" |
| Compartilhamento (users/groups permissionados num registro) | mode="pivot" |
| Anexos de uma mensagem/tarefa | mode="pivot" |
| Cadastro de tags/categorias do sistema (tela de config) | mode="flat" |
| Lista lateral de filtros dinamicos (ex: sidebar com tags master) | mode="flat" |
| Qualquer CRUD inline de master data sem parent | mode="flat" |
Slot <mad-db-blocks-form> — form de criacao inline
Conteudo livre — qualquer componente MAD. Filhos viram o body do form disparado pelo botao "+ Adicionar". Os campos sao enviados via mad_model[...] e o framework preenche $this->form do host antes de chamar blockAdd.
Contrato do row-view Blade partial
Recebe:
$item— model Eloquent da pivot (relacionamentos disponiveis:$item->familia->nome)$state— token encriptado (usar emmad:click/mad:change, nunca escrever o atributo compiladodata-mad-click/data-mad-changea mao)$name— nome do bloco (pra IDs DOM unicos)
blockAdd/blockRemove/blockUpdate sempre re-renderizam a lista inteira
(substituem o innerHTML de #mad-db-blocks-list-{name}, o <span> que
envolve todas as linhas) — nao existe patch por linha individual. Por isso um
id="block-row-{$name}-{$item->id}" no elemento raiz do partial nao e
exigido pelo framework para o re-render funcionar; mantenha-o mesmo assim por
boa pratica (debug, CSS de transicao, scroll-into-view custom), so nao conte
com ele pra um update parcial.
Acoes dentro da linha usam os primitivos do trait:
mad:click="blockRemove({{ $item->id }},'{{ $state }}')"— remove 1 registroblockUpdate(int $id, string $field, mixed $value, string $state)— update 1 coluna, mas nao dá pra chamar direto viamad:change:mad-livewire.jssempre anexa o valor do elemento como o último argumento posicional (params.push(el.value)), e aqui$valueprecisa cair na 3ª posicao (antes de$state, que é a 4ª). A solucao é expor um método-ponte no host que recebe os args na ordem que omad:changerealmente envia (id, field, state, novoValor) e repassa pro trait na ordem certa — ver exemplo abaixo (onRoleChange). Nunca tente passarthis.valueliteral dentro do atributo: o parser de argumentos do MadWire rodaJSON.parse()no conteúdo entre parênteses, não avalia JS.
Exemplo 1 — Compartilhamento (lista de users + role editavel)
View
@php
$roles = ['manage' => 'Gerenciar', 'edit' => 'Editar', 'view' => 'Visualizar'];
@endphp
<mad-db-blocks name="shares"
pivot-model="GedDocumentShare"
database="communication"
foreign-key="document_id"
row-view="ged.share-row"
order-by="created_at"
add-mode="modal"
add-label="+ Compartilhar"
empty-text="Nenhum compartilhamento"
on-add="onShareAdd">
<mad-db-blocks-form>
<mad-dbseek-field name="principal_id" label="Usuario"
model="SystemUsers" display="name" required />
<mad-select-field name="role" label="Permissao" :options="$roles" required />
</mad-db-blocks-form>
</mad-db-blocks>
Partial app/resources/views/ged/share-row.blade.php
@php
$roles = ['manage' => 'Gerenciar', 'edit' => 'Editar', 'view' => 'Visualizar'];
@endphp
<div class="mad-db-blocks-row" id="block-row-{{ $name }}-{{ $item->id }}">
<mad-avatar :name="$item->principal->name" size="sm" />
<div style="flex:1;">
<div>{{ $item->principal->name }}</div>
<div class="muted">{{ $item->origem }}</div>
</div>
<select mad:change="onRoleChange({{ $item->id }},'role','{{ $state }}')">
@foreach($roles as $v => $l)
<option value="{{ $v }}" @selected($item->role === $v)>{{ $l }}</option>
@endforeach
</select>
<button mad:click="blockRemove({{ $item->id }},'{{ $state }}')">×</button>
</div>
Controller
use Mad\Component\MadComponent;
use Mad\Form\MadDbBlocksTrait;
use Mad\Http\MadResponse;
class GedDocumentDetail extends MadComponent
{
use MadDbBlocksTrait;
public ?int $registroId = null;
/**
* Hook on-add — preenche campos que nao vem do form
*/
public function onShareAdd(GedDocumentShare $share): void
{
$share->created_by = session('userid');
$share->created_at = date('Y-m-d H:i:s');
}
// Ponte pro trait: mad:change entrega (id, field, state, valor) — nessa
// ordem, porque "valor" vem sempre por ultimo (auto-append do JS) — e
// repassa pro blockUpdate() do trait na ordem que ELE espera.
public function onRoleChange(int $id, string $field, string $state, string $value): MadResponse
{
return $this->blockUpdate($id, $field, $value, $state);
}
}
Exemplo 2 — Links compartilhaveis (geracao server-side sem form)
Quando nao ha campos pro usuario digitar, o form pode ter apenas configuracao minima e o hook on-add gera o token:
<mad-db-blocks name="links"
pivot-model="GedDocumentShareLink"
database="communication"
foreign-key="document_id"
row-view="ged.link-row"
add-mode="popover"
add-label="+ Criar novo link"
on-add="onLinkCreate">
<mad-db-blocks-form>
<mad-select-field name="expires_in" label="Expira em"
:options="['1d' => '1 dia', '7d' => '7 dias', '30d' => '30 dias', null => 'Nunca']" />
</mad-db-blocks-form>
</mad-db-blocks>
public function onLinkCreate(GedDocumentShareLink $link): void
{
$link->token = bin2hex(random_bytes(16));
$link->created_by = session('userid');
if ($link->expires_in) {
$link->expires_at = date('Y-m-d H:i:s', strtotime('+' . $link->expires_in));
}
}
Exemplo 3 — Attach de items existentes (form com dbcombo/dbseek)
Para anexar items de uma tabela mestre (ex: vincular produtos a um pedido), declare um <mad-db-blocks-form> com <mad-dbcombo-field> ou <mad-dbseek-field> apontando para o model master:
<mad-db-blocks name="produtos"
pivot-model="PedidoProduto"
foreign-key="pedido_id"
row-view="pedido.produto-row"
add-mode="modal"
add-label="+ Adicionar produto">
<mad-db-blocks-form>
<mad-dbseek-field name="produto_id" label="Produto"
model="Produto" display="nome" required />
</mad-db-blocks-form>
</mad-db-blocks>
O form preenche $pivot->produto_id automaticamente; o trait seta a FK a partir do record-id do parent.
Hooks — convencao de retorno
Todos os hooks (on-add, on-remove, on-update) seguem a mesma convencao:
| Retorno | Trait faz |
|---|---|
void / null / true |
Segue fluxo: persiste + re-render |
false |
Aborta, rollback, toast de erro |
MadResponse |
Retorna diretamente (controle total — permite save multi-tabela, redirect, etc) |
// Hook minimo
public function onShareAdd(GedDocumentShare $s): void
{
$s->created_by = session('userid');
}
// Hook com validacao
public function onShareAdd(GedDocumentShare $s): bool|MadResponse
{
if ($s->principal_id == session('userid')) {
return MadMessage::error('Erro', 'Nao pode compartilhar consigo mesmo');
}
$s->created_by = session('userid');
return true;
}
// Hook com controle total
public function onShareAdd(GedDocumentShare $s): MadResponse
{
// faz tudo manualmente — save em multiplas tabelas, notificar, etc
$s->created_by = session('userid');
$s->save();
(new NotificationService())->notify($s->principal_id, 'compartilhado');
return (new MadResponse())
->toast('Compartilhado e notificado!', 'success')
->closeModal('db-blocks-shares-modal');
}
Hook pos-commit (on-after-add / on-after-remove)
on-add/on-remove/on-update rodam dentro da DB::transaction() — se
precisar de algo que so pode acontecer depois do commit (ex: manageRow, que
abre uma conexao PDO propria), use on-after-add/on-after-remove. Recebem o
pivot ja persistido + o MadResponse que sera devolvido, pra acrescentar ops:
<mad-db-blocks name="shares" pivot-model="GedDocumentShare"
foreign-key="document_id" row-view="ged.share-row"
on-add="onShareAdd" on-after-add="onShareAdded">
...
</mad-db-blocks>
public function onShareAdd(GedDocumentShare $s): void
{
$s->created_by = session('userid');
}
public function onShareAdded(GedDocumentShare $s, MadResponse $resp): void
{
// commit ja aplicado — seguro pra side-effects que abrem outra conexao
$resp->toast('Compartilhamento salvo e processado!', 'success');
}
Caso especial — tags coloridas com chips
Para tags (padrao chip colorido), use o partial reutilizavel components.partials.tag-chip como row-view. O form cria a tag master via on-add:
<mad-db-blocks name="tags"
pivot-model="GedDocumentTag" database="communication"
foreign-key="document_id" :record-id="$docId"
row-view="components.partials.tag-chip"
add-mode="popover" add-label="+ Tag"
on-add="onTagCreate">
<mad-db-blocks-form>
<mad-input-field name="name" label="Nome" required />
<mad-color-field name="color" label="Cor" />
</mad-db-blocks-form>
</mad-db-blocks>
No controller host, o on-add cria o master GedTag a partir do form e seta $pivot->tag_id:
public function onTagCreate(GedDocumentTag $pivot): void
{
$data = $_POST['mad_model'] ?? [];
$tag = new GedTag();
$tag->name = trim($data['name'] ?? '');
$tag->color = $data['color'] ?? '#a78bfa';
$tag->created_by = session('userid');
$tag->save();
$pivot->tag_id = $tag->id;
}
NUNCA fazer
{{-- ERRADO: usar componente sem o trait no controller — "Metodo nao permitido: blockAdd" --}}
class MeuForm extends MadComponent { /* sem use MadDbBlocksTrait */ }
{{-- CERTO --}}
class MeuForm extends MadComponent {
use \Mad\Form\MadDbBlocksTrait;
}
{{-- EVITAR: row-view sem id no elemento raiz — dificulta debug/CSS, mesmo sem ser exigido pelo framework --}}
<div class="row">{{ $item->nome }}</div>
{{-- PREFERIR: id estavel por boa pratica (nao usado pelo blockUpdate, que sempre re-renderiza a lista) --}}
<div class="row" id="block-row-{{ $name }}-{{ $item->id }}">{{ $item->nome }}</div>
{{-- ERRADO: recarregar tudo apos add/remove/update --}}
public function onShareAdd(...) {
$this->loadData(); // full re-render
}
{{-- CERTO: deixar o trait cuidar do re-render parcial --}}
public function onShareAdd(GedDocumentShare $s): void
{
$s->created_by = session('userid');
// trait faz save() + renderList + html() automaticamente
}
{{-- ERRADO: prefixar metodo do trait com _ — handler do framework bloqueia --}}
public function _blockBeforeAdd() { ... }
{{-- CERTO: usar on-add no Blade + metodo publico sem prefixo _ --}}
on-add="onShareAdd"
public function onShareAdd(...) { ... }
{{-- ERRADO: montar HTML da lista manualmente com @foreach no host view --}}
@foreach($shares as $s)
<div>...</div>
@endforeach
{{-- CERTO: usar db-blocks com row-view --}}
<mad-db-blocks name="shares" pivot-model="..." row-view="ged.share-row" />