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:

SuperficieCampo metadata (SQL)Firma callbackRitorno 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.mcuigridcolumndatatemplatescope: 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)booleanreturn 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 gridmcgrdcndtonaltemplatecondition (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 salvataggiomdbeforesave / 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.

Snippet 1js
// Titolo = "Modifica '<CityName>'"
return `Modifica '${record.CityName.value}'`;

Altri esempi reali:

Snippet 2js
// 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' }}
  • *ngIf block: <ng-container *ngIf="...">...</ng-container>
  • Tag HTML: <span>, <i class='pi pi-...'>, <strong>, ecc.
Snippet 3HTML
<!-- 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>
Snippet 4HTML
<!-- concatena nome cognome -->
<span>{{ rowData.first_name }} {{ rowData.last_name }}</span>
Snippet 5HTML
<!-- badge condizionale -->
<span class='badge'>{{ rowData.stato === 0 ? 'Bozza' : rowData.stato === 1 ? 'Confermato' : 'Annullato' }}</span>
Snippet 6HTML
<!-- 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.

Snippet 7js
// Concatena due campi in un'unica cella
return record.first_name.value + ' ' + record.last_name.value;
Snippet 8js
// 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.

Snippet 9js
record[field.mc_nome_colonna] = new Date().toISOString().slice(0, 10);
Snippet 10js
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 valorevalidateResult nello scope.

Snippet 11js
if (!record[field.mc_nome_colonna].value && record["colonna_numero"].value) {
  vr.message = "Campo obbligatorio"; return false;
}
return true;
Snippet 12js
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.

Snippet 13js
// 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).

Snippet 14js
// 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}`;
}
Snippet 15js
// 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 è un BehaviorSubject → leggi con record.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 }) (mai window.confirm).

Configurazione SQL della colonna button:

Snippet 16SQL
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).

Snippet 17js
// 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;
  }
Snippet 18js
// 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) o datasource.getSelectedKeys() (solo PK).
  • Spinner globale: wtoolbox.isBusy.next(true/false).
  • disablecallback può ritornare true per 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__tabellemustattributename = classe CSS, mustattributevalue = condizione JS che ritorna boolean. La classe è applicata alla riga solo quando la condizione è true.

Snippet 19js
// 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
Snippet 20js
// 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))
Snippet 21js
// row-success: attività completata
record && Number(record.completed ?? 0) === 1

Configurazione SQL (riga rossa quando scaduta):

Snippet 22SQL
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:

  • mustattributename contiene solo la classe CSS (es. row-danger); la condizione sta in mustattributevalue e deve ritornare esplicitamente true/false.
  • Le classi row-danger / row-warning / row-success sono 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.

Snippet 23js
// md_before_save: forza maiuscolo sul codice + timestamp
record.codice.next((record.codice.value || '').toUpperCase());
Snippet 24js
// 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

SintomoCausaFix
Il titolo/cella si svuotausato .next() (setter) per leggereleggi con .value / .getValue()
datasource.refresh is not a functionmetodo canonico è fetchData()usa datasource.fetchData()
Toast non comparehelper inesistente (showToastSuccess)wtoolbox.messageNotificationService.add({severity,summary,detail})
Azione toolbar non apparemdmultipleselection=0UPDATE ... SET mdmultipleselection=1
Update SQL ignoratousato l'alias runtime invece del nome SQLusa il nome SQL reale (es. mcbuttonaction, non mc_button_action)
Stile non applicatomustattributevalue non ritorna booleanassicurati che la condizione ritorni true/false

Vedi anche