| name | frappe-client-script-logic |
| description | Implement dynamic form behavior, field dependencies, and client-side validations in Frappe using JavaScript. Use for interactive UI logic within DocTypes. |
Frappe Client Script Logic
Handle dynamic UI interactions, field visibility, and complex client-side validations.
Capabilities
1. Conditional Visibility & Requirements
Pattern: Dynamic Field Toggles
frappe.ui.form.on('Payment Entry', {
payment_type: function(frm) {
let is_receive = frm.doc.payment_type === 'Receive';
let is_pay = frm.doc.payment_type === 'Pay';
let is_transfer = frm.doc.payment_type === 'Internal Transfer';
frm.toggle_display('paid_from', is_pay || is_transfer);
frm.toggle_display('paid_to', is_receive || is_transfer);
frm.toggle_reqd('paid_from', is_pay);
frm.toggle_reqd('paid_to', is_receive);
}
});
2. Smart Defaults & Auto-Fill
Pattern: Data Fetching on Change
frappe.ui.form.on('Sales Invoice', {
customer: function(frm) {
if (frm.doc.customer) {
frappe.call({
method: 'erpnext.accounts.party.get_party_details',
args: {
party: frm.doc.customer,
party_type: 'Customer'
},
callback: function(r) {
if (r.message) {
frm.set_value('customer_name', r.message.customer_name);
frm.set_value('territory', r.message.territory);
}
}
});
}
}
});
3. Client-Side Validation
frappe.ui.form.on('My DocType', {
validate: function(frm) {
if (frm.doc.start_date > frm.doc.end_date) {
frappe.msgprint(__('Start Date cannot be after End Date'));
frappe.validated = false;
}
}
});
References
Decision Tree & Reference
Source skill: frappe-impl-clientscripts (Frappe Claude Skill Package workspace). Condensed workflows: client vs server, event choice, hardened rules, and pitfalls called out there.
Client vs server (must logic always runโAPI/import/console?)
MUST the logic ALWAYS run (imports, API, bulk import, bench console)?
โโโ YES โ implement on the server (controller / server script)
โโโ NO โ goal-based split
โโโ UX / instant feedback โ client script
โโโ Show-hide / dynamic DF โ client script
โโโ Link filtering โ client script
โโโ Data validation โ BOTH (client UX + server integrity)
โโโ Derived numbers โ mirror on client when helpful, reconcile on server if business-critical
Choose the right form event
Need โ handler
โโโ Link filters โ setup (runs once earliest)
โโโ Custom buttons โ refresh (rebuilt each render)
โโโ Show/hide / mandatory toggles โ refresh **and** {fieldname} (initial + interactive)
โโโ Block bad saves โ validate (frappe.throw)
โโโ Follow-up once saved โ after_save
โโโ Recalculate on edits โ {fieldname}
โโโ Grid row lifecycle โ {table}_add / {table}_remove / child field handlers
โโโ One-shot init before data render โ setup **or** onload (see syntax skill for nuance)
โโโ Needs fully rendered DOM โ onload_post_render
Server-call decision tree
โโโ Single field lookup on one doc โ frappe.db.get_value (promise; light)
โโโ Method on current document โ frm.call (controller must be @frappe.whitelist)
โโโ Any other whitelisted function โ frappe.call ({ method, args }) โ inspect r.message
โโโ Prefer promise-only ergonomics โ frappe.xcall (same whitelist rules)
Performance guardrails (frappe-impl-clientscripts):
| Rule | Rationale |
|---|
set_query only in setup | Avoid duplicate registration each refresh |
Batch frm.set_value({ ... }) | Fewer redraw passes |
| Cache heavy reads | e.g., frm._cache_key = โฆ rather than repeating calls |
| NEVER server calls inside tight loops without batching | Prefer one batched whitelist method + map |
ALWAYS / NEVER (workflow level)
- ALWAYS treat client validation as UXโpair with authoritative server checks whenever data integrity matters.
- ALWAYS migrate UI > Client Script UI workflows to
hooks.py โ doctype_js when scripts exceed ~50 lines, need CI, multi-site deploy, or team review.
- For conditional visibility/requirements, ALWAYS combine
refresh (initial paint) and the driving {fieldname} handler and optionally frm.trigger('field') from refresh to sync startup stateโnever rely on only one of them.
- ALWAYS clear dependent Link fields after their parent filters change (
frm.set_value('child_link', '')).
- ALWAYS use
flt() for arithmetic on Doc values to absorb null/undefined safely.
- ALWAYS recalculate totals on
{table}_remove, not only on field edits.
- NEVER rely on
frappe.msgprint alone to abort savesโcall frappe.throw (or validated async awaited checks) inside validate when persistence must stop (frappe.validated = false is brittle vs throw).
- NEVER add transactional buttons inside
setup/onloadโrefresh is where UI chrome is reconstructed.
- NEVER block
validate with slow server hops unless UX expectations are set (async validate still waits user-facing latency).
- ALWAYS
await networked checks in validate; letting callbacks finish later means the doc may already save.
Custom-button workflow rules:
| ALWAYS | NEVER |
|---|
Re-add buttons in refresh | Define action buttons purely in setup/onload |
Check frm.is_new() plus docstatus | Offer actions that assume a persisted name without guards |
Wrap labels via __() | Ship English-only literals |
Anti-patterns (from impl guidance)
These are shorthand โwhat bites teamsโ distilled from workflow sectionsโsee frappe-syntax-clientscripts for fuller API pitfalls.
| Pitfall | Why it fails |
|---|
Only refresh or only {fieldname} visibility toggles | Form loads stale or skips live edits |
set_query in refresh | Re-registers unnecessarily; violates perf guidance |
refresh_field('items') inside per-row loops after every frappe.model.set_value during bulk edits | Repeated grid rendersโtouch rows then refresh once unless framework auto-handles |
Unguarded frm.add_custom_button | Duplicate buttons or actions on unsubmitted drafts/cancelled docs |
validate firing frappe.call via callback stack | Race lets save slip through |
| Long client scripts stranded in Desk | No review history / environment parity |
Clearing totals without {table}_remove hook | Row deletions silently desync aggregates |