Overview
Callback & metadata cookbook
Ricettario di callback configurabili sui metadati WUIC: piccoli snippet JavaScript salvati su un campo metadata di tabella/colonna che il framework esegue a runtime per ottenere un effetto specifico (titolo dinamico, validazione, azione di riga, riga colorata, ecc.).
Ogni ricetta è nella forma Effetto desiderato → campo metadata → snippet → note. Gli snippet sono editabili dal metadata-editor (la maggior parte ha un code editor con autocomplete) oppure via SQL diretto sulla tabella metadata indicata.
Contratto dei callback (firme)
Ogni callback riceve un set fisso di argomenti a seconda della superficie. Tabella di riferimento:
| Superficie | Campo metadata (SQL) | Firma callback | Ritorno atteso |
|---|---|---|---|
| Titolo del form (display formula tabella) | _metadati__tabelle.mddisplayformula | (metaInfo, record, datasource, wtoolbox) | string (il titolo) |
| Valore mostrato di una colonna (display template) | _metadati__colonne.mcuigridcolumndatatemplate | scope: rowData (oggetto riga raw) | template Angular markup |
| Valore di default di una colonna | _metadati__colonne.mcdefaultvaluecallback | (record, field, metaInfo, wtoolbox) | scrivi record[field.mc_nome_colonna] = valore (il return è ignorato) |
| Validazione custom | _metadati__colonne.mc_validation_custom_callback | (record, field, vr, wtoolbox) | boolean — return false blocca il save; setta vr.message per il testo |
| Selezione cambiata (lookup/select) | _metadati__colonne.mcslctionchangedcustomfunction | (record, value, datasource, wtoolbox) | — (side-effect) |
| Condizione template condizionale grid | mcgrdcndtonaltemplatecondition (colonna) / mdgrdcndtonaltemplatecondition (tabella) | (record, ...) | boolean |
| Azione di riga (button col) | _metadati__colonne.mcbuttonaction (con voa_class=6) | (datasource, record, event, field, wtoolbox) | — (side-effect) |
| Azione toolbar (bulk) | _mtdt__cstom__actions__tabelle.actioncallback | (datasource, metaInfo, record, event, wtoolbox) | — (side-effect) |
| Disabilita azione toolbar | _mtdt__cstom__actions__tabelle.disablecallback | (datasource, metaInfo, record, event, wtoolbox) | boolean (true = disabilitata) |
| Stile condizionale riga/cella (tabella) | _metadati__u_i__stili__tabelle.mustattributevalue (+ mustattributename = classe CSS) | (record / dataItem) | boolean (true = applica la classe) |
| Stile condizionale colonna | _metadati__u_i__stili__colonne.musc_attribute_value (+ musc_attribute_name) | (record / dataItem) | boolean |
| Lifecycle salvataggio | mdbeforesave / mdaftersave / mdafterload | (record, datasource, wtoolbox) | — (side-effect) |
> Nota campi reattivi: nel record ogni campo è un BehaviorSubject<T>. Si legge con record.<colonna>.value (o .getValue()); si scrive con record.<colonna>.next(nuovoValore). Per i dettagli vedi Field widgets.
> Nota metadata SQL: i nomi nelle clausole SET/WHERE sono i nomi SQL reali (es. mcbuttonaction, voa_class, mustattributevalue), non gli alias runtime. Vedi Metadata integration.
---
1. Titolo dinamico del form di edit
Effetto: l'header del form di edit mostra un titolo calcolato dal record (es. "Modifica 'Roma'") invece del nome route.
Campo: _metadati__tabelle.mddisplayformula.
// Titolo = "Modifica '<CityName>'"
return `Modifica '${record.CityName.value}'`;Altri esempi reali:
// Etichetta composta da una descrizione lunga
return 'Cliente [' + record.ragione_sociale.value + ']';
// Titolo statico per una route "nuovo"
return 'Nuova pagina';Note: leggi sempre con .value. .next() NON va usato qui: scriverebbe nel campo (svuotandolo) e ritornerebbe void → titolo vuoto.
---
2. Valore mostrato di una colonna (display template Angular)
Effetto: la cella in lista mostra un valore derivato/formattato invece del raw (il valore in DB rimane invariato — è solo presentazione).
Campo: _metadati__colonne.mcuigridcolumndatatemplate (template Angular markup processato da list-grid.component.ts:buildGridColumnTemplateSwitchCases).
Scope: la variabile rowData (oggetto riga raw, NON BehaviorSubject — accesso diretto: rowData.<col>).
Sintassi: markup Angular template, NON body JS. Supporta:
- Interpolation
{{ }}con espressioni inline - Pipe standard Angular:
number:'1.1-1',date:'dd/MM/yyyy',currency:'EUR',percent,uppercase,lowercase,slice - Ternario inline:
{{ rowData.x > 0 ? 'pos' : 'neg' }} *ngIfblock:<ng-container *ngIf="...">...</ng-container>- Tag HTML:
<span>,<i class='pi pi-...'>,<strong>, ecc.
<!-- popolazione formato compatto k/M -->
<span>{{ rowData.population >= 1000000 ? (rowData.population / 1000000 | number:'1.1-1') + 'M' : rowData.population >= 1000 ? (rowData.population / 1000 | number:'1.1-1') + 'k' : rowData.population }}</span><!-- concatena nome cognome -->
<span>{{ rowData.first_name }} {{ rowData.last_name }}</span><!-- badge condizionale -->
<span class='badge'>{{ rowData.stato === 0 ? 'Bozza' : rowData.stato === 1 ? 'Confermato' : 'Annullato' }}</span><!-- data formattata -->
<span>{{ rowData.created_at | date:'dd/MM/yyyy' }}</span>> Attenzioni:
> - mc_display_string_in_view e mc_display_string_in_edit sono campi distinti che fanno il label dell'header della colonna in lista/form, NON una display formula. Scrivere un template li' produce un header rotto.
> - mccomputedclientformula e mccomputedformula esistono nei modelli C#/TS ma sono dead code lato Angular runtime (mai cablati al render). Non usarli per il display.
> - Il template DEVE contenere almeno un < o {{ per essere riconosciuto come Angular markup; un testo plain viene ignorato dal framework.
// Concatena due campi in un'unica cella
return record.first_name.value + ' ' + record.last_name.value;// Badge testuale in base a uno stato numerico
const s = Number(record.stato.value ?? 0);
return s === 0 ? 'Bozza' : (s === 1 ? 'Confermato' : 'Annullato');Note: ritorna sempre una string. Per HTML colorato nella cella usa invece i template condizionali (sezione 7) o gli stili condizionali (sezione 8) — la display formula è testo.
---
3. Valore di default di un campo
Effetto: in inserimento il campo è precompilato (data odierna, utente corrente, valore derivato).
Campo: _metadati__colonne.mcdefaultvaluecallback. Firma: (record, field, metaInfo, wtoolbox).
> ⚠️ Il callback DEVE scrivere il valore nel record: record[field.mc_nome_colonna] = <valore>. Il return è ignorato dal framework (DataSourceComponent.addNewRecord usa solo ciò che viene scritto nel record). In fase di add-new-record il record è un plain object (non ancora BehaviorSubject): assegna direttamente.
record[field.mc_nome_colonna] = new Date().toISOString().slice(0, 10);record[field.mc_nome_colonna] = wtoolbox.userInfoService.getuserInfo().user_id;Note: per l'utente corrente usa sempre UserInfoService (mai leggere cookie/localStorage direttamente). NON usare return: il valore va scritto in record[field.mc_nome_colonna].
---
4. Validazione custom di un campo
Effetto: blocca il salvataggio con un messaggio se il valore non rispetta una regola che dipende da altri campi.
Campo: _metadati__colonne.mc_validation_custom_callback. Firma: (record, field, vr, wtoolbox) → ritorna boolean.
> ⚠️ return false BLOCCA il salvataggio; return true valida. Setta vr.message = '...' per il testo d'errore mostrato. Leggi i valori con record[field.mc_nome_colonna].value (qui il record è reattivo, BehaviorSubject). NON esistono valore né validateResult nello scope.
if (!record[field.mc_nome_colonna].value && record["colonna_numero"].value) {
vr.message = "Campo obbligatorio"; return false;
}
return true;if (Number(record[field.mc_nome_colonna].value) < 0) {
vr.message = "Il valore non può essere negativo"; return false;
}
return true;Note: l'esito si comunica con il return boolean (false blocca il save) + vr.message per il messaggio. Per messaggi user-facing localizzati vedi UI localization.
---
5. Azione al cambio selezione (lookup / select)
Effetto: quando l'utente cambia il valore di un lookup, ricalcola/precompila altri campi (es. scelto il cliente, riempi partita IVA e listino).
Campo: _metadati__colonne.mcslctionchangedcustomfunction.
// Al cambio del lookup "cliente" copia la P.IVA nel record corrente
const cliente = record.cliente__lookup_obj?.value;
if (cliente) {
record.partita_iva.next(cliente.partita_iva ?? '');
record.listino_id.next(cliente.listino_id ?? null);
}Note: l'oggetto lookup risolto è in record.<colonna>__lookup_obj.value. Qui .next() è corretto: stai scrivendo negli altri campi.
---
6. Azione di riga (button nel dropdown della riga)
Effetto: ogni riga ha un'azione (apri dettaglio, stampa, invia, converti, ...).
Campo: colonna virtuale in _metadati__colonne con mc_ui_column_type='button' + voa_class=6; il codice sta in mcbuttonaction.
Firma: (datasource, record, event, field, wtoolbox).
// Naviga a un'altra route filtrando per l'id del padre (esempio reale FlottaMezzi)
async function (datasource, record, event, field, wtoolbox) {
const prodId = Number(record.prodotto_id?.value ?? record.prodotto_id);
if (!prodId) return;
window.location.hash = `#/prodotti/edit/${prodId}`;
}// Chiama un endpoint sul record + toast + refresh grid (esempio reale Fatturazione)
async function (datasource, record, event, field, wtoolbox) {
const id = Number(record.id?.value ?? record.id);
if (!id) {
wtoolbox.messageNotificationService.add({ severity: 'error', summary: 'Errore', detail: 'Record non valido' });
return;
}Note:
record.idè unBehaviorSubject→ leggi conrecord.id?.value.- Per refreshare la grid usa `datasource.fetchData()` (NON
refresh(), non esiste). - Per i toast usa `wtoolbox.messageNotificationService.add({severity, summary, detail})`.
- Per un prompt di conferma usa
wtoolbox.promptDialog?.({ header, message })(maiwindow.confirm).
Configurazione SQL della colonna button:
INSERT INTO _metadati__colonne (
md_id, mc_nome_colonna, mc_ui_column_type, mc_display_string_in_view,
voa_class, mcbuttonaction, mc_button_image, mc_ordine, mc_hide_in_edit
) VALUES (
<md_id>, 'btn_invia', 'button', 'Invia',
6, '<JS body>', 'pi pi-send', 999, 1
);---
7. Azione toolbar (bulk su righe selezionate)
Effetto: un button "Azioni" sopra la grid che opera sulle righe selezionate via checkbox (es. Marca pagate, Esporta selezionati, Genera solleciti).
Campo: _mtdt__cstom__actions__tabelle.actioncallback.
Firma: (datasource, metaInfo, record, event, wtoolbox).
// Bulk su selezione + endpoint + toast + refresh (pattern canonico)
async function (datasource, metaInfo, record, event, wtoolbox) {
const selected = (datasource.getSelectedRows && datasource.getSelectedRows()) || [];
if (!selected.length) {
wtoolbox.messageNotificationService.add({ severity: 'warn', summary: 'Selezione vuota', detail: 'Seleziona almeno una riga' });
return;
}// Azione che non opera su selezione: lancia un processo server e mostra l'esito (esempio reale "Scaffold OData")
wtoolbox.isBusy.next(true);
var res = await (wtoolbox.http.get(wtoolbox.appSettings.meta_url + 'ScaffoldOData').toPromise());
wtoolbox.isBusy.next(false);
wtoolbox.messageNotificationService.add({ severity: 'success', summary: 'OK', detail: res?.message || 'Completato' });Note:
- Per abilitare i checkbox di selezione la tabella deve avere
mdmultipleselection=1(_metadati__tabelle). datasource.getSelectedRows()(oggetti) odatasource.getSelectedKeys()(solo PK).- Spinner globale:
wtoolbox.isBusy.next(true/false). disablecallbackpuò ritornaretrueper disabilitare l'item (es. nessuna riga selezionata).
Vedi la skill operativa Custom actions per la procedura completa (backend + metadata + test).
---
8. Righe / celle colorate condizionalmente (stili)
Effetto: evidenzia righe in base a una condizione (scadute in rosso, in attesa in giallo, completate in verde).
Campo: _metadati__u_i__stili__tabelle — mustattributename = classe CSS, mustattributevalue = condizione JS che ritorna boolean. La classe è applicata alla riga solo quando la condizione è true.
// row-danger: opportunità scaduta e ancora aperta (esempio reale CRM)
record && record.expected_close_date
&& (new Date(record.expected_close_date).getTime() < Date.now())
&& Number(record.stato ?? 0) === 0// row-warning: lead non aggiornato da più di 7 giorni (esempio reale CRM)
record && record.updated_at
&& ((Date.now() - new Date(record.updated_at).getTime()) > (7 * 24 * 60 * 60 * 1000))// row-success: attività completata
record && Number(record.completed ?? 0) === 1Configurazione SQL (riga rossa quando scaduta):
INSERT INTO _metadati__u_i__stili__tabelle (mdid, mustattributename, mustattributevalue)
VALUES (<md_id>, 'row-danger', 'return record && record.due_date && new Date(record.due_date).getTime() < Date.now();');Note:
mustattributenamecontiene solo la classe CSS (es.row-danger); la condizione sta inmustattributevaluee deve ritornare esplicitamentetrue/false.- Le classi
row-danger/row-warning/row-successsono già stilizzate dal framework; per classi custom aggiungi il CSS nella board/app. - Per lo stile a livello di singola colonna/cella usa
_metadati__u_i__stili__colonne(musc_attribute_name+musc_attribute_value), con la stessa semantica.
---
9. Lifecycle del record (before/after save, after load)
Effetto: normalizza dati prima del salvataggio, ricalcola campi dopo il load, applica regole condizionali a update/delete.
Campi (_metadati__tabelle): mdbeforesave, mdaftersave, mdafterload, mdconditionalupdaterule, mdconditionaldeleterule.
// md_before_save: forza maiuscolo sul codice + timestamp
record.codice.next((record.codice.value || '').toUpperCase());// md_after_load: calcola un campo derivato non persistito dopo il caricamento
const tot = Number(record.imponibile.value ?? 0) + Number(record.iva.value ?? 0);
record.totale.next(tot);Note: questi callback girano nel contesto del record reattivo; usa .value per leggere e .next() per scrivere. Per logica server-side pesante preferisci un endpoint/stored procedure invece del callback client.
---
Trappole comuni
| Sintomo | Causa | Fix |
|---|---|---|
| Il titolo/cella si svuota | usato .next() (setter) per leggere | leggi con .value / .getValue() |
datasource.refresh is not a function | metodo canonico è fetchData() | usa datasource.fetchData() |
| Toast non compare | helper inesistente (showToastSuccess) | wtoolbox.messageNotificationService.add({severity,summary,detail}) |
| Azione toolbar non appare | mdmultipleselection=0 | UPDATE ... SET mdmultipleselection=1 |
| Update SQL ignorato | usato l'alias runtime invece del nome SQL | usa il nome SQL reale (es. mcbuttonaction, non mc_button_action) |
| Stile non applicato | mustattributevalue non ritorna boolean | assicurati che la condizione ritorni true/false |
Vedi anche
- Custom actions — procedura completa azioni toolbar/riga.
- Field widgets — campi reattivi e widget.
- Metadata integration — modello dei metadati tabelle/colonne.
- List grid — la griglia su cui agiscono azioni e stili.