App Studio
Custom JavaScript Examples
Real-world patterns for using Custom JS in App Studio — computed summaries, dynamic chart data, conditional routing, and custom table sorting.
Example 1: Computed Summary Stats from DataGrid
Attach this to a DataGrid widget's onDataChange event to compute totals and averages that display in Stat widgets on the same page:
// DataGrid widget — onDataChange
function onDataChange(widget, variables, actions, context) {
const rows = widget.data?.rows ?? [];
const total = rows.reduce((s, r) => s + (r.dealValue ?? 0), 0);
const avg = rows.length > 0 ? total / rows.length : 0;
const highValue = rows.filter(r => r.dealValue > 50000).length;
variables.set('dealTotal', total);
variables.set('dealAverage', Math.round(avg));
variables.set('highValueCount', highValue);
}
// Stat widgets on the same page display:
// {{ variables.dealTotal | currency }}
// {{ variables.dealAverage | currency }}
// {{ variables.highValueCount }}
Example 2: Custom Table Sorting
When the built-in column sort is not sufficient (e.g., natural-order string sort), use Custom JS to pre-sort data and write it to a variable that the grid binds to:
// DataGrid — onDataChange — natural sort on "version" field
function onDataChange(widget, variables, actions, context) {
const rows = [...(widget.data?.rows ?? [])];
// Natural sort: "2.10.0" > "2.9.0" (unlike lexicographic)
rows.sort(function(a, b) {
return a.version.localeCompare(b.version, undefined, { numeric: true });
});
variables.set('sortedRows', rows);
}
// DataGrid config — binds rows to the sorted variable
// "dataSource": "variables.sortedRows"
Example 3: Dynamic Chart Configuration
Transform flat data into a chart-ready series format on the fly:
// Chart widget — onDataChange
function onDataChange(widget, variables, actions, context) {
const rows = widget.data?.rows ?? [];
// Group by month
const byMonth = {};
rows.forEach(function(r) {
const month = r.closedDate.substring(0, 7); // "2026-01"
byMonth[month] = (byMonth[month] ?? 0) + r.dealValue;
});
const categories = Object.keys(byMonth).sort();
const values = categories.map(m => byMonth[m]);
variables.set('chartCategories', categories);
variables.set('chartSeries', [{ name: 'Revenue', data: values }]);
}
// Chart widget binds:
// categories: {{ variables.chartCategories }}
// series: {{ variables.chartSeries }}
Example 4: Role-Conditional Navigation After Row Click
// DataGrid — onUserAction (row click)
function onUserAction(widget, variables, actions, context) {
const row = widget.selectedRow;
if (!row) return;
const isAdmin = context.roles.includes('admin');
const isOwner = row.assignedUserId === context.userId;
if (isAdmin || isOwner) {
// Admins and owners go to the edit page
actions.navigate('/leads/' + row.id + '/edit');
} else {
// Others go to the read-only detail page
actions.navigate('/leads/' + row.id);
}
}
Example 5: Computed Form Validation
// Form widget — onUserAction (before submit)
function onUserAction(widget, variables, actions, context) {
const values = widget.formValues;
// Custom cross-field validation: end date must be after start date
const start = new Date(values.startDate);
const end = new Date(values.endDate);
if (end <= start) {
variables.set('formError', 'End date must be after start date');
actions.showNotification('End date must be after start date', 'warning');
return; // Stop — don't proceed to submit
}
variables.set('formError', null);
// Proceed — the no-code submit action chain fires after this custom JS
}
Example 6: Selecting All Rows and Triggering Bulk Action
// "Select All + Archive" button — onUserAction
function onUserAction(widget, variables, actions, context) {
const rows = widget.data?.rows ?? [];
const activeIds = rows
.filter(r => r.status === 'active')
.map(r => r.id);
if (activeIds.length === 0) {
actions.showNotification('No active records to archive', 'info');
return;
}
actions.triggerWorkflow('BulkArchive', {
ids: activeIds,
archivedBy: context.userId
}).then(function(result) {
actions.showNotification(
result.archivedCount + ' records archived',
'success'
);
});
}