Client-Side Recipes
Small desk JavaScript patterns, such as filling a dialog's Select field from a query and pushing messages from Python to the browser.
On this page
These are short, reusable snippets for desk-side JavaScript: form scripts, page scripts, and bundles loaded through app_include_js. Each one is self-contained; copy the one you need and swap in your own doctypes and method paths.
Fill a Dialog's Select Field from a Query
Use this when a dialog needs a dropdown built from live data, for example letting a user map an imported line to an existing Item or create a new one.
Build the dialog with Frappe's field definitions rather than injecting raw HTML. Then load the options and set them with set_df_property:
function map_item_dialog(callback) {
const dialog = new frappe.ui.Dialog({
title: __("Map Item"),
fields: [
{
fieldtype: "HTML",
fieldname: "help",
options: `<div class="alert alert-info">${__(
"Pick an existing item, or leave it blank to create a new one."
)}</div>`,
},
{ fieldtype: "Select", fieldname: "item", label: __("Existing Item") },
{ fieldtype: "Section Break", label: __("New Item") },
{ fieldtype: "Data", fieldname: "description", label: __("Description") },
{ fieldtype: "Currency", fieldname: "standard_rate", label: __("Standard Rate") },
],
primary_action_label: __("Save"),
async primary_action(values) {
if (values.item) {
callback(values.item);
} else {
const item = await frappe.xcall("<APP_NAME>.api.create_item", {
description: values.description,
standard_rate: values.standard_rate,
});
callback(item);
}
dialog.hide();
},
});
frappe.db
.get_list("Item", { fields: ["name", "item_name"], filters: { disabled: 0 }, limit: 500 })
.then((items) => {
dialog.set_df_property("item", "options", [
"",
...items.map((item) => ({ value: item.name, label: item.item_name })),
]);
});
dialog.show();
dialog.get_field("item").$input.trigger("focus");
}Select options can be plain strings or
{ value, label }objects. The leading empty string gives the user a blank choice.frappe.db.get_listreturns 20 rows unless you passlimit. Set a ceiling that fits the data.<APP_NAME>.api.create_itemstands for a@frappe.whitelist()method in your app that creates the record and returns its name.
Tip
For more than a few hundred records, use a Link field (fieldtype: "Link", options: "Item") instead of a Select. It searches on the server as the user types, respects permissions, and you can narrow it with get_query.
Push Messages from Python to the Browser
Frappe's realtime server lets server code send events to the browser of a specific user, a document's viewers, or everyone. Use it to report progress from background jobs or to debug server code that runs where you can't see print output.
On the server, publish an event:
frappe.publish_realtime(
"console_log",
{"message": f"Adding punches for {employee}"},
user=frappe.session.user,
)In the browser (a form script, or a bundle loaded on every page), subscribe to it:
frappe.realtime.on("console_log", (data) => {
console.log(data.message);
});Call frappe.realtime.off("console_log") when the listener is no longer needed, such as in a page's on_page_hide.
Useful options and built-in events:
user=sends to one user's sessions;doctype=anddocname=send to everyone viewing that document; with none of them, the event goes to every desk user on the site.after_commit=Trueholds the event until the transaction commits, so the browser never hears about a change that was rolled back.Publishing to the built-in
msgprintevent shows a message dialog without any client code:frappe.publish_realtime("msgprint", "Import finished", user=user). Withoutuser=, it goes to the current session user.frappe.publish_progress(percent, title="Importing")drives the built-in progress bar.
Note
Older notes mention frappe.emit_js or an eval_js realtime event for running arbitrary JavaScript from Python. Neither exists in current Frappe. Publish a named event and handle it explicitly, as above.
Sources
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.