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.
frappe.msgprint
Section titled “frappe.msgprint”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.
// --- 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,});frappe.confirm
Section titled “frappe.confirm”A Yes/No modal. Pass a message and a “yes” callback; the optional third argument is the “no” callback.
// --- 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' }); });frappe.prompt
Section titled “frappe.prompt”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.
// --- 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'));frappe.show_progress
Section titled “frappe.show_progress”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.
// --- 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.frappe.ui.Dialog — advanced patterns
Section titled “frappe.ui.Dialog — advanced patterns”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.
// --- 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();}frappe.ui.FilterGroup
Section titled “frappe.ui.FilterGroup”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().
// --- 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;}frappe.new_doc with pre-filled fields
Section titled “frappe.new_doc with pre-filled fields”Open a brand-new form with fields already populated. frappe.new_doc handles
top-level fields; child tables need a server-side helper.
// --- Navigate to a new form with fields pre-populated ---
// Simple: just pass field valuesfrappe.new_doc('Ice Cream Order', { franchise_outlet: 'SJ-KOR-001', customer: 'Walk-In Customer', order_date: frappe.datetime.nowdate(),});
// With child table rows pre-populatedfrappe.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);}frappe.set_route with query parameters
Section titled “frappe.set_route with query parameters”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.
// --- 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();Quick reference table
Section titled “Quick reference table”| Component | Use Case | Returns |
|---|---|---|
frappe.msgprint(msg) | Display info/error messages | void |
frappe.confirm(msg, yes, no) | Yes/No confirmation | void (callbacks) |
frappe.prompt(fields, cb, title, btn) | Quick input collection | void (callback with values) |
frappe.show_progress(title, count, total) | Progress bar | void |
frappe.show_alert({message, indicator}, seconds) | Toast notification | void |
frappe.ui.Dialog({...}) | Complex modal forms | Dialog 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 form | void |
frappe.set_route(...) | Navigate anywhere | void |
frappe.dom.freeze(msg) / unfreeze() | Block UI during operations | void |
frappe.utils.debounce(fn, ms) | Debounce a function | Function |