Overview
Callback & metadata cookbook
Cookbook of callbacks configurable on WUIC metadata: small JavaScript snippets saved on a table/column metadata field that the framework executes at runtime to achieve a specific effect (dynamic title, validation, row action, colored row, etc.).
Each recipe is in the form Desired effect → metadata field → snippet → notes. Snippets are editable from the metadata-editor (most have a code editor with autocomplete) or via direct SQL on the indicated metadata table.
Callback contract (signatures)
Each callback receives a fixed set of arguments depending on the surface. Reference table:
| Surface | Metadata field (SQL) | Callback signature | Expected return |
|---|---|---|---|
| Form title (table display formula) | _metadati__tabelle.mddisplayformula | (record, field, metaInfo, wtoolbox) | write record[field.mc_nome_colonna] = value (the return is ignored) |
| Displayed value of a column (display template) | _metadati__colonne.mcuigridcolumndatatemplate | scope: rowData (raw row object) | Angular template markup |
| Default value of a column | _metadati__colonne.mcdefaultvaluecallback | (record, field, metaInfo, wtoolbox) | default value |
| Custom validation | _metadati__colonne.mc_validation_custom_callback | (record, field, vr, wtoolbox) | boolean — return false blocks the save; set vr.message for the text |
| Selection changed (lookup/select) | _metadati__colonne.mcslctionchangedcustomfunction | (record, value, datasource, wtoolbox) | — (side-effect) |
| Conditional grid template condition | mcgrdcndtonaltemplatecondition (column) / mdgrdcndtonaltemplatecondition (table) | (record, ...) | boolean |
| Row action (button col) | _metadati__colonne.mcbuttonaction (with voa_class=6) | (datasource, record, event, field, wtoolbox) | — (side-effect) |
| Toolbar action (bulk) | _mtdt__cstom__actions__tabelle.actioncallback | (datasource, metaInfo, record, event, wtoolbox) | — (side-effect) |
| Disable toolbar action | _mtdt__cstom__actions__tabelle.disablecallback | (datasource, metaInfo, record, event, wtoolbox) | boolean (true = disabled) |
| Conditional row/cell style (table) | _metadati__u_i__stili__tabelle.mustattributevalue (+ mustattributename = CSS class) | (record / dataItem) | boolean (true = apply the class) |
| Conditional column style | _metadati__u_i__stili__colonne.musc_attribute_value (+ musc_attribute_name) | (record / dataItem) | boolean |
| Save lifecycle | mdbeforesave / mdaftersave / mdafterload | (record, datasource, wtoolbox) | — (side-effect) |
> Reactive fields note: in the record each field is a BehaviorSubject<T>. You read with record.<colonna>.value (or .getValue()); you write with record.<colonna>.next(nuovoValore). For details see Field widgets.
> Metadata SQL note: names in the SET/WHERE clauses are the real SQL names (e.g. mcbuttonaction, voa_class, mustattributevalue), not the runtime aliases. See Metadata integration.
---
1. Dynamic title of the edit form
Effect: the edit form header shows a title computed from the record (e.g. "Modifica 'Roma'") instead of the route name.
Field: _metadati__tabelle.mddisplayformula.
// Titolo = "Modifica '<CityName>'"
return `Modifica '${record.CityName.value}'`;Other real examples:
// Etichetta composta da una descrizione lunga
return 'Cliente [' + record.ragione_sociale.value + ']';
// Titolo statico per una route "nuovo"
return 'Nuova pagina';Notes: always read with .value. .next() must NOT be used here: it would write to the field (emptying it) and return void → empty title.
---
2. Displayed value of a column (display formula)
Effect: the list cell shows a derived/formatted value instead of the raw one (the DB value is unchanged — display only).
Field: _metadati__colonne.mcuigridcolumndatatemplate (Angular template markup processed by list-grid.component.ts:buildGridColumnTemplateSwitchCases).
Scope: variable rowData (raw row object, NOT BehaviorSubject — direct access: rowData.<col>).
Syntax: Angular template markup, NOT JS body. Supports:
- Interpolation
{{ }}with inline expressions - Standard Angular pipes:
number:'1.1-1',date:'dd/MM/yyyy',currency:'EUR',percent,uppercase,lowercase,slice - Inline ternary:
{{ rowData.x > 0 ? 'pos' : 'neg' }} *ngIfblock:<ng-container *ngIf="...">...</ng-container>- HTML tags:
<span>,<i class='pi pi-...'>,<strong>, etc.
<!-- 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>> Warnings:
> - mc_display_string_in_view and mc_display_string_in_edit are distinct fields that hold the column header label in list/form, NOT a display formula. Writing a template there produces a broken header.
> - mccomputedclientformula and mccomputedformula exist in the C#/TS models but are dead code on the Angular runtime side (never wired to the render). Don't use them for display.
> - The template MUST contain at least one < or {{ to be recognized as Angular markup; plain text is ignored by the 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');Notes: always return a string. For colored HTML in the cell use the conditional templates (section 7) or the conditional styles (section 8) instead — the display formula is text.
---
3. Default value of a field
Effect: on insert the field is pre-filled (today's date, current user, derived value).
Field: _metadati__colonne.mcdefaultvaluecallback.
// Data odierna in formato ISO yyyy-MM-dd
record[field.mc_nome_colonna] = new Date().toISOString().slice(0, 10);// Utente corrente come responsabile di default
record[field.mc_nome_colonna] = wtoolbox.userInfoService.getuserInfo().user_id;Notes: for the current user always use UserInfoService (never read cookie/localStorage directly).
---
4. Custom validation of a field
Effect: blocks the save with a message if the value does not satisfy a rule that depends on other fields.
Field: _metadati__colonne.mc_validation_custom_callback.
// "Obbligatorio se l'altro campo è valorizzato" (esempio reale)
if (!record[field.mc_nome_colonna].value && record["colonna_numero"].value) {
vr.message = "Campo obbligatorio"; return false;
}
return true;// Vincolo di range
if (Number(record[field.mc_nome_colonna].value) < 0) {
vr.message = "Il valore non può essere negativo"; return false;
}
return true;Notes: the outcome is returned as a boolean (false blocks the save) plus vr.message for the text. Read the value with record[field.mc_nome_colonna].value. There is no valore nor validateResult in scope. For localized user-facing messages see UI localization.
---
5. Action on selection change (lookup / select)
Effect: when the user changes the value of a lookup, recompute/pre-fill other fields (e.g. once the customer is chosen, fill in VAT number and price list).
Field: _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);
}Notes: the resolved lookup object is in record.<colonna>__lookup_obj.value. Here .next() is correct: you are writing to the other fields.
---
6. Row action (button in the row dropdown)
Effect: each row has an action (open detail, print, send, convert, ...).
Field: virtual column in _metadati__colonne with mc_ui_column_type='button' + voa_class=6; the code lives in mcbuttonaction.
Signature: (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;
}
Notes:
record.idis aBehaviorSubject→ read withrecord.id?.value.- To refresh the grid use `datasource.fetchData()` (NOT
refresh(), it does not exist). - For toasts use `wtoolbox.messageNotificationService.add({severity, summary, detail})`.
- For a confirmation prompt use
wtoolbox.promptDialog?.({ header, message })(neverwindow.confirm).
SQL configuration of the button column:
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. Toolbar action (bulk on selected rows)
Effect: an "Actions" button above the grid that operates on the rows selected via checkbox (e.g. Mark as paid, Export selected, Generate reminders).
Field: _mtdt__cstom__actions__tabelle.actioncallback.
Signature: (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' });Notes:
- To enable the selection checkboxes the table must have
mdmultipleselection=1(_metadati__tabelle). datasource.getSelectedRows()(objects) ordatasource.getSelectedKeys()(PK only).- Global spinner:
wtoolbox.isBusy.next(true/false). disablecallbackcan returntrueto disable the item (e.g. no row selected).
See the operational skill Custom actions for the complete procedure (backend + metadata + test).
---
8. Conditionally colored rows / cells (styles)
Effect: highlight rows based on a condition (overdue in red, pending in yellow, completed in green).
Field: _metadati__u_i__stili__tabelle — mustattributename = CSS class, mustattributevalue = JS condition that returns a boolean. The class is applied to the row only when the condition is 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) === 1SQL configuration (red row when overdue):
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();');Notes:
mustattributenamecontains only the CSS class (e.g.row-danger); the condition lives inmustattributevalueand must explicitly returntrue/false.- The classes
row-danger/row-warning/row-successare already styled by the framework; for custom classes add the CSS in the board/app. - For styling at the level of a single column/cell use
_metadati__u_i__stili__colonne(musc_attribute_name+musc_attribute_value), with the same semantics.
---
9. Record lifecycle (before/after save, after load)
Effect: normalize data before saving, recompute fields after load, apply conditional rules on update/delete.
Fields (_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);Notes: these callbacks run in the context of the reactive record; use .value to read and .next() to write. For heavy server-side logic prefer an endpoint/stored procedure instead of the client callback.
---
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| The title/cell empties out | used .next() (setter) to read | read with .value / .getValue() |
datasource.refresh is not a function | the canonical method is fetchData() | use datasource.fetchData() |
| Toast does not appear | nonexistent helper (showToastSuccess) | wtoolbox.messageNotificationService.add({severity,summary,detail}) |
| Toolbar action does not appear | mdmultipleselection=0 | UPDATE ... SET mdmultipleselection=1 |
| SQL update ignored | used the runtime alias instead of the SQL name | use the real SQL name (e.g. mcbuttonaction, not mc_button_action) |
| Style not applied | mustattributevalue does not return a boolean | make sure the condition returns true/false |
See also
- Custom actions — complete procedure for toolbar/row actions.
- Field widgets — reactive fields and widgets.
- Metadata integration — metadata model for tables/columns.
- List grid — the grid on which actions and styles operate.