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:

SurfaceMetadata field (SQL)Callback signatureExpected 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.mcuigridcolumndatatemplatescope: 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)booleanreturn 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 conditionmcgrdcndtonaltemplatecondition (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 lifecyclemdbeforesave / 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.

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

Other real examples:

Snippet 2js
// 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' }}
  • *ngIf block: <ng-container *ngIf="...">...</ng-container>
  • HTML tags: <span>, <i class='pi pi-...'>, <strong>, etc.
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>

> 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.

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');

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.

Snippet 9js
// Data odierna in formato ISO yyyy-MM-dd
record[field.mc_nome_colonna] = new Date().toISOString().slice(0, 10);
Snippet 10js
// 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.

Snippet 11js
// "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;
Snippet 12js
// 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.

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);
}

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).

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;
  }

Notes:

  • record.id is a BehaviorSubject → read with record.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 }) (never window.confirm).

SQL configuration of the button column:

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. 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).

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' });

Notes:

  • To enable the selection checkboxes the table must have mdmultipleselection=1 (_metadati__tabelle).
  • datasource.getSelectedRows() (objects) or datasource.getSelectedKeys() (PK only).
  • Global spinner: wtoolbox.isBusy.next(true/false).
  • disablecallback can return true to 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__tabellemustattributename = CSS class, mustattributevalue = JS condition that returns a boolean. The class is applied to the row only when the condition is 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

SQL configuration (red row when overdue):

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();');

Notes:

  • mustattributename contains only the CSS class (e.g. row-danger); the condition lives in mustattributevalue and must explicitly return true/false.
  • The classes row-danger / row-warning / row-success are 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.

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);

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

SymptomCauseFix
The title/cell empties outused .next() (setter) to readread with .value / .getValue()
datasource.refresh is not a functionthe canonical method is fetchData()use datasource.fetchData()
Toast does not appearnonexistent helper (showToastSuccess)wtoolbox.messageNotificationService.add({severity,summary,detail})
Toolbar action does not appearmdmultipleselection=0UPDATE ... SET mdmultipleselection=1
SQL update ignoredused the runtime alias instead of the SQL nameuse the real SQL name (e.g. mcbuttonaction, not mc_button_action)
Style not appliedmustattributevalue does not return a booleanmake sure the condition returns true/false

See also