$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-syntax-clientscriptsUse when writing client-side JavaScript for ERPNext/Frappe form events, field manipulation, server calls, or child table handling in v14/v15/v16. Covers exact syntax for frappe.ui.form.on, frm methods, frappe.call, and browser-side validation. Keywords: client script, form event,
| 1 | # Frappe Client Scripts Syntax |
| 2 | |
| 3 | Client Scripts run in the browser and control all UI interactions in Frappe/ERPNext. Create them via **Setup > Client Script** or in custom apps under `public/js/`. |
| 4 | |
| 5 | **CRITICAL**: Client Script validations ONLY apply in the browser form view. API calls and System Console bypass them. ALWAYS pair with Server Scripts for security-critical validation. |
| 6 | |
| 7 | ## Quick Reference |
| 8 | |
| 9 | | Action | Code | |
| 10 | |--------|------| |
| 11 | | Set value | `frm.set_value('field', value)` | |
| 12 | | Get value | `frm.doc.fieldname` | |
| 13 | | Hide field | `frm.toggle_display('field', false)` | |
| 14 | | Make mandatory | `frm.toggle_reqd('field', true)` | |
| 15 | | Make read-only | `frm.toggle_enable('field', false)` | |
| 16 | | Set field property | `frm.set_df_property('field', 'options', [...])` | |
| 17 | | Filter Link field | `frm.set_query('field', () => ({filters: {}}))` | |
| 18 | | Call server | `frappe.call({method: 'path.to.fn', args: {}})` | |
| 19 | | Call doc method | `frm.call('method_name', {args})` | |
| 20 | | Prevent save | `frappe.throw(__('Error message'))` | |
| 21 | | Add button | `frm.add_custom_button(__('Label'), callback, group)` | |
| 22 | | Add child row | `frm.add_child('table', {values}); frm.refresh_field('table')` | |
| 23 | | Show alert | `frappe.show_alert({message: __('Done'), indicator: 'green'})` | |
| 24 | | Translate string | `__('Text')` or `__('Hello {0}', [name])` | |
| 25 | |
| 26 | ## Event Decision Tree |
| 27 | |
| 28 | ``` |
| 29 | What do you need to do? |
| 30 | │ |
| 31 | ├─ One-time setup (queries, formatters)? |
| 32 | │ └─ ALWAYS use setup — runs once per form instance |
| 33 | │ |
| 34 | ├─ Show/hide fields, add buttons, update UI? |
| 35 | │ └─ ALWAYS use refresh — fires after every load/reload |
| 36 | │ |
| 37 | ├─ Validate data before save? |
| 38 | │ └─ ALWAYS use validate — use frappe.throw() to block save |
| 39 | │ |
| 40 | ├─ Modify data right before server save? |
| 41 | │ └─ Use before_save — last chance to change values |
| 42 | │ |
| 43 | ├─ Run logic after successful save? |
| 44 | │ └─ Use after_save — document is persisted |
| 45 | │ |
| 46 | ├─ React to a field value change? |
| 47 | │ └─ Use the fieldname as the event name |
| 48 | │ |
| 49 | ├─ Intercept workflow state change? |
| 50 | │ └─ Use before_workflow_action / after_workflow_action |
| 51 | │ |
| 52 | └─ Manipulate DOM after full render? |
| 53 | └─ Use onload_post_render — NEVER use jQuery selectors directly |
| 54 | ``` |
| 55 | |
| 56 | > See [references/events.md](references/events.md) for complete event list and execution order. |
| 57 | |
| 58 | ## Form Event Registration |
| 59 | |
| 60 | ```javascript |
| 61 | // Parent form events |
| 62 | frappe.ui.form.on('Sales Order', { |
| 63 | setup(frm) { }, // Once per form instance |
| 64 | refresh(frm) { }, // After every load/reload |
| 65 | validate(frm) { }, // Before save — throw to block |
| 66 | fieldname(frm) { } // On field value change |
| 67 | }); |
| 68 | |
| 69 | // Child table events — ALWAYS register on the CHILD doctype |
| 70 | frappe.ui.form.on('Sales Order Item', { |
| 71 | qty(frm, cdt, cdn) { |
| 72 | let row = frappe.get_doc(cdt, cdn); |
| 73 | frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate); |
| 74 | }, |
| 75 | items_add(frm, cdt, cdn) { }, // Row added |
| 76 | items_remove(frm) { }, // Row removed (no cdt/cdn) |
| 77 | items_move(frm) { } // Row reordered |
| 78 | }); |
| 79 | ``` |
| 80 | |
| 81 | ## Value Manipulation |
| 82 | |
| 83 | ```javascript |
| 84 | // ALWAYS use frm.set_value() — NEVER assign frm.doc.field directly |
| 85 | frm.set_value('status', 'Approved'); // Single |
| 86 | frm.set_value({status: 'Approved', priority: 'High'}); // Multiple |
| 87 | |
| 88 | // Read values (read-only — NEVER write via frm.doc) |
| 89 | let val = frm.doc.fieldname; |
| 90 | let items = frm.doc.items; // Child table array |
| 91 | ``` |
| 92 | |
| 93 | ## Field Properties |
| 94 | |
| 95 | ```javascript |
| 96 | // Show/hide (accepts single field or array) |
| 97 | frm.toggle_display(['priority', 'due_date'], frm.doc.status === 'Open'); |
| 98 | |
| 99 | // Mandatory toggle |
| 100 | frm.toggle_reqd('due_date', true); |
| 101 | |
| 102 | // Read-only toggle |
| 103 | frm.toggle_enable('amount', false); // false = read-only |
| 104 | |
| 105 | // Arbitrary property change |
| 106 | frm.set_df_property('status', 'options', ['New', 'Open', 'Closed']); |
| 107 | frm.set_df_property('amount', 'read_only', 1); |
| 108 | frm.set_df_property('notes', 'label', 'Internal Notes'); |
| 109 | |
| 110 | // Intro message at form top |
| 111 | frm.set_intro('This document is pending review', 'orange'); |
| 112 | ``` |
| 113 | |
| 114 | ## Link Field Filters |
| 115 | |
| 116 | ```javascript |
| 117 | // ALWAYS set queries in setup event — NEVER in refresh |
| 118 | frappe.ui.form.on('Sales Order', { |
| 119 | setup(frm) { |
| 120 | // Simple filter |
| 121 | frm.set_query('customer', () => ({ |
| 122 | filters: { disabled: 0 } |
| 123 | })); |
| 124 | |
| 125 | // Child table filter |
| 126 | frm.set_query('item_code', 'items', (doc, cdt, cdn) => { |
| 127 | let row = locals |