Skip to content

Frappe UI Component Recipes

Problem: You keep reaching for the same handful of Frappe UI primitives — messages, confirmations, prompts, progress bars, dialogs, routing — and want one quick reference with working, copy-pasteable examples for the ScoopJoy desk.

Solution: Below is each major Frappe UI component with a runnable snippet you can drop into any client script, form script, or even the browser console.

The workhorse for surfacing information. Pass a string for a plain message, or an options object for a titled, color-coded message — optionally with a primary action or a full HTML body.

Use in any client script, form script, or browser console
// --- Simple message ---
frappe.msgprint('Order #ICO-2025-0042 has been placed.');
// --- With indicator and title ---
frappe.msgprint({
title: __('Low Stock Alert'),
indicator: 'orange', // green, blue, orange, red, grey, darkgrey
message: __('Vanilla ice cream stock is below 50 litres at <b>ScoopJoy Koramangala</b>.'),
});
// --- With primary action ---
frappe.msgprint({
title: __('Order Confirmed'),
indicator: 'green',
message: __('Order <b>ICO-2025-0042</b> confirmed. Total: {0}',
[format_currency(1250, 'INR')]),
primary_action: {
label: __('Print Receipt'),
server_action: 'scoopjoy.api.orders.print_receipt',
args: { order: 'ICO-2025-0042' },
// OR client-side action:
// action: () => { window.print(); }
},
});
// --- Wide message with HTML table ---
frappe.msgprint({
title: __('Stock Summary'),
indicator: 'blue',
message: `
<table class="table table-sm table-bordered">
<thead>
<tr><th>Flavour</th><th>Stock (L)</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>Vanilla</td><td>45</td><td class="text-danger">Low</td></tr>
<tr><td>Chocolate</td><td>120</td><td class="text-success">OK</td></tr>
<tr><td>Strawberry</td><td>80</td><td class="text-success">OK</td></tr>
</tbody>
</table>`,
wide: true,
});

A Yes/No modal. Pass a message and a “yes” callback; the optional third argument is the “no” callback.

Use in any client script or form script
// --- Standard confirm ---
frappe.confirm(
__('Delete all expired menu items from this outlet?'),
() => {
// Yes callback
frappe.xcall('scoopjoy.api.menu.delete_expired', { outlet: cur_frm.doc.name })
.then(() => {
frappe.show_alert({ message: 'Expired items deleted', indicator: 'green' });
cur_frm.reload_doc();
});
},
() => {
// No callback (optional)
frappe.show_alert({ message: 'Cancelled', indicator: 'grey' });
}
);

Collect input without building a full form. Pass a single field spec or an array of them; the callback receives a values object keyed by fieldname.

Use in any client script or form script
// --- Single field prompt ---
frappe.prompt(
{ fieldname: 'reason', fieldtype: 'Small Text', label: 'Suspension Reason', reqd: 1 },
(values) => {
frappe.xcall('scoopjoy.api.franchise.suspend_outlet', {
outlet: cur_frm.doc.name,
reason: values.reason,
}).then(() => cur_frm.reload_doc());
},
__('Suspend Outlet'),
__('Confirm Suspension')
);
// --- Multi-field prompt ---
frappe.prompt(
[
{ fieldname: 'target_revenue', fieldtype: 'Currency', label: 'Monthly Target (INR)',
reqd: 1, default: 250000 },
{ fieldname: 'target_orders', fieldtype: 'Int', label: 'Monthly Order Target',
reqd: 1, default: 500 },
{ fieldname: 'effective_date', fieldtype: 'Date', label: 'Effective From',
reqd: 1, default: frappe.datetime.month_start() },
],
(values) => {
frappe.xcall('scoopjoy.api.franchise.set_targets', {
outlet: cur_frm.doc.name,
...values,
}).then(() => {
frappe.show_alert({ message: 'Targets updated', indicator: 'green' });
});
},
__('Set Monthly Targets'),
__('Save Targets')
);

Drive a progress bar for long client-side loops. Call it once per iteration with the current count and total; pair it with frappe.hide_progress() when done.

Use in any client script or form script
// --- Determinate progress (known total) ---
async function sync_menu_prices(frm) {
const items = frm.doc.menu_items || [];
const total = items.length;
for (let i = 0; i < total; i++) {
frappe.show_progress(
'Syncing Prices',
i + 1,
total,
`Updating ${items[i].item_name}...`
);
await frappe.xcall('scoopjoy.api.menu.sync_item_price', {
outlet: frm.doc.name,
item_code: items[i].item_code,
});
}
frappe.show_progress('Syncing Prices', total, total, 'Complete!');
setTimeout(() => frappe.hide_progress(), 1500);
frm.reload_doc();
}
// --- Indeterminate progress (from server push) ---
// Server side:
// frappe.publish_realtime('progress', {'progress': [30, 100], 'title': 'Processing...'})
// This auto-displays a progress bar on the client; no extra client code needed.

When frappe.prompt isn’t enough, build a frappe.ui.Dialog directly. An HTML field lets you inject arbitrary markup; a Table field gives you an editable in-place grid right inside the modal.

Use in any client script or custom page
// --- Dialog with custom HTML content ---
function show_outlet_comparison(outlets) {
const d = new frappe.ui.Dialog({
title: 'Outlet Comparison',
size: 'extra-large',
fields: [
{
fieldname: 'comparison_html',
fieldtype: 'HTML',
}
],
});
const html = `
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead class="bg-light">
<tr>
<th>Metric</th>
${outlets.map(o => `<th>${o.name}</th>`).join('')}
</tr>
</thead>
<tbody>
<tr>
<td>Revenue</td>
${outlets.map(o => `<td>${format_currency(o.revenue)}</td>`).join('')}
</tr>
<tr>
<td>Orders</td>
${outlets.map(o => `<td>${o.orders}</td>`).join('')}
</tr>
<tr>
<td>Rating</td>
${outlets.map(o => `<td>${o.rating}</td>`).join('')}
</tr>
</tbody>
</table>
</div>
`;
d.fields_dict.comparison_html.$wrapper.html(html);
d.show();
}
// --- Dialog with DataTable (editable grid) ---
async function show_bulk_price_editor(outlet) {
const items = await frappe.xcall('scoopjoy.api.menu.get_outlet_prices', { outlet });
const d = new frappe.ui.Dialog({
title: `Edit Prices: ${outlet}`,
size: 'extra-large',
fields: [
{
fieldname: 'prices',
fieldtype: 'Table',
label: 'Menu Prices',
cannot_add_rows: true,
in_place_edit: true,
data: items,
fields: [
{ fieldname: 'item_code', fieldtype: 'Data', label: 'Item Code',
in_list_view: 1, read_only: 1, columns: 2 },
{ fieldname: 'item_name', fieldtype: 'Data', label: 'Item',
in_list_view: 1, read_only: 1, columns: 3 },
{ fieldname: 'current_rate', fieldtype: 'Currency', label: 'Current Rate',
in_list_view: 1, read_only: 1, columns: 2 },
{ fieldname: 'new_rate', fieldtype: 'Currency', label: 'New Rate',
in_list_view: 1, columns: 2 },
{ fieldname: 'change_pct', fieldtype: 'Percent', label: '% Change',
in_list_view: 1, read_only: 1, columns: 1 },
],
},
],
primary_action_label: 'Update All Prices',
async primary_action(values) {
const changes = values.prices.filter(r => r.new_rate && r.new_rate !== r.current_rate);
if (!changes.length) {
frappe.msgprint('No price changes detected.');
return;
}
frappe.dom.freeze('Updating prices...');
await frappe.xcall('scoopjoy.api.menu.bulk_update_prices', {
outlet,
prices: changes.map(r => ({
item_code: r.item_code,
new_rate: r.new_rate,
})),
});
frappe.dom.unfreeze();
d.hide();
frappe.show_alert({ message: `${changes.length} prices updated`, indicator: 'green' });
},
});
d.show();
}

Attach Frappe’s standard filter panel to a custom page so users get the same filter UI they know from list views — then read the filters back via get_filters().

Use in a custom page
// --- Programmatic filter panel ---
function add_filter_panel(page, doctype, callback) {
const filter_group = new frappe.ui.FilterGroup({
parent: page.page_actions,
doctype: doctype,
filter_button: page.page_actions.find('.filter-button'),
on_change: () => {
const filters = filter_group.get_filters();
callback(filters);
},
});
// Set default filters programmatically
filter_group.add_filters_to_filter_group([
[doctype, 'status', '=', 'Active'],
[doctype, 'franchise_type', '=', 'Premium'],
]);
return filter_group;
}

Open a brand-new form with fields already populated. frappe.new_doc handles top-level fields; child tables need a server-side helper.

Use in any client script or form script
// --- Navigate to a new form with fields pre-populated ---
// Simple: just pass field values
frappe.new_doc('Ice Cream Order', {
franchise_outlet: 'SJ-KOR-001',
customer: 'Walk-In Customer',
order_date: frappe.datetime.nowdate(),
});
// With child table rows pre-populated
frappe.route_options = {
franchise_outlet: 'SJ-KOR-001',
customer: 'Walk-In Customer',
};
frappe.set_route('Form', 'Ice Cream Order', 'new');
// Note: frappe.new_doc cannot pre-fill child tables.
// For child table pre-population, use a server-side helper:
async function create_reorder(outlet, items) {
const doc = await frappe.xcall('scoopjoy.api.orders.create_reorder', {
outlet, items,
});
// Navigate to the newly created (unsaved) doc
frappe.set_route('Form', 'Ice Cream Order', doc.name);
}

The single entry point for desk navigation — forms, filtered lists, reports, custom pages, and workspaces. Read the current route with frappe.get_route() and listen for changes via frappe.router.

Use in any client script or custom page
// --- Route to a form ---
frappe.set_route('Form', 'Franchise Outlet', 'SJ-KOR-001');
// --- Route to a list with filters ---
frappe.set_route('List', 'Franchise Outlet', {
franchise_type: 'Premium',
status: 'Active',
city: 'Bangalore',
});
// --- Route to a report ---
frappe.set_route('query-report', 'Franchise Sales Summary', {
franchise_outlet: 'SJ-KOR-001',
from_date: '2025-01-01',
to_date: '2025-01-31',
});
// --- Route to a custom page ---
frappe.set_route('franchise-command-center');
// --- Route to workspace ---
frappe.set_route('Workspaces', 'ScoopJoy');
// --- Read current route ---
const current = frappe.get_route();
// Returns array: ['Form', 'Franchise Outlet', 'SJ-KOR-001']
// --- Listen for route changes ---
frappe.router.on('change', () => {
const route = frappe.get_route();
console.log('Navigated to:', route.join('/'));
});
// --- Go back ---
frappe.router.back();
ComponentUse CaseReturns
frappe.msgprint(msg)Display info/error messagesvoid
frappe.confirm(msg, yes, no)Yes/No confirmationvoid (callbacks)
frappe.prompt(fields, cb, title, btn)Quick input collectionvoid (callback with values)
frappe.show_progress(title, count, total)Progress barvoid
frappe.show_alert({message, indicator}, seconds)Toast notificationvoid
frappe.ui.Dialog({...})Complex modal formsDialog instance
frappe.xcall(method, args)Server call (promise)Promise<any>
frappe.call({method, args, callback})Server call (callback)Promise
frappe.new_doc(doctype, values)Navigate to pre-filled formvoid
frappe.set_route(...)Navigate anywherevoid
frappe.dom.freeze(msg) / unfreeze()Block UI during operationsvoid
frappe.utils.debounce(fn, ms)Debounce a functionFunction