Form Scripts
Form Scripts lets you add client side logic to your Forms. You can write Form Scripts for automatically fetching values, adding validation or adding contextual actions to your Form.
Standard Form Scripts
When you create a new DocType, a
{doctype}.js
is created where you can write your form script.
Syntax:
frappe.ui.form.on(doctype, {
event1() {
// handle event 1
},
event2() {
// handle event 2
}
})
For example,
todo.js
which is located at
frappe/desk/doctype/todo/todo.js
may look like this:
// Script for ToDo Form
frappe.ui.form.on('ToDo', {
// on refresh event
refresh(frm) {
// if reference_type and reference_name are set,
// add a custom button to go to the reference form
if (frm.doc.reference_type && frm.doc.reference_name) {
frm.add_custom_button(__(frm.doc.reference_name), () => {
frappe.set_route("Form", frm.doc.reference_type, frm.doc.reference_name);
});
}
}
})
Child Table Scripts
Child Table Scripts should be written in the same file as their parent.
frappe.ui.form.on('Quotation', {
// ...
})
frappe.ui.form.on('Quotation Item', {
// cdt is Child DocType name i.e Quotation Item
// cdn is the row name for e.g bbfcb8da6a
item_code(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
}
})
Custom Form Scripts
You can also write form scripts by creating Client Script in the system. You should write Client Scripts if the logic is specific to your site. If you want to share Form Scripts across sites, you must include them via Apps.
To create a new Client Script, go to
Home > Customization > Client Script > New
New Client Script for Form
Form Events
Form Scripts depend on events to trigger. Here are the list of all Form Events that are triggered by Form.
These events will get
frm
as the first parameter in their handler functions.
frappe.ui.form.on('ToDo', {
// frm passed as the first parameter
setup(frm) {
// write setup code
}
})
Event Name |
Description |
---|---|
|
Triggered once when the form is created for the first time |
|
Triggered before the form is about to load |
|
Triggered when the form is loaded and is about to render |
|
Triggered when the form is loaded and rendered. |
|
Triggered after the form is loaded and rendered |
|
Triggered before before_save |
|
Triggered before save is called |
|
Triggered after form is saved |
|
Triggered before submit is called |
|
Triggered after form is submitted |
|
Triggered before cancel is called |
|
Triggered after form is cancelled |
|
Triggered before discard is called |
|
Triggered after form is discarded |
|
Triggered after form timeline is rendered |
|
Triggered when a row is opened as a form in a Table field |
|
Triggered when the value of fieldname is changed |
|
Called by the email dialog to fetch default filters for email recipients. Should accept two parameters
|
|
Called by the email dialog to fetch default recipients. Should accept two parameters
|
Child Table Events
These events are triggered in the context of a Child Table. Hence, along with
frm
, they will also get the
cdt
(Child DocType) and
cdn
(Child Docname) parameters in their handler functions.
Imagine our "ToDo" DocType has a field called "links" that contains a Child Table. This Child Table is defined in a DocType called "Dynamic Link". We want our code to run whenever a row is added to the table.
// this code is located inside `todo.js`
frappe.ui.form.on('Dynamic Link', { // The child table is defined in a DoctType called "Dynamic Link"
links_add(frm, cdt, cdn) { // "links" is the name of the table field in ToDo, "_add" is the event
// frm: current ToDo form
// cdt: child DocType 'Dynamic Link'
// cdn: child docname (something like 'a6dfk76')
// cdt and cdn are useful for identifying which row triggered this event
frappe.msgprint('A row has been added to the links table 🎉 ');
}
});
Event Name |
Description |
---|---|
|
Triggered when a row is about to be removed from a Table field |
|
Triggered when a row is added to a Table field |
|
Triggered when a row is removed from a Table field |
|
Triggered when a row is reordered to another location in a Table field |
|
Triggered when a row is opened as a form in a Table field |
Form API
Here are a list of common methods that are available on the
frm
object.
frm.set_value
Set the value of a field. This will trigger the field change event in the form.
// set a single value
frm.set_value('description', 'New description')
// set multiple values at once
frm.set_value({
status: 'Open',
description: 'New description'
})
// returns a promise
frm.set_value('description', 'New description')
.then(() => {
// do something after value is set
})
frm.refresh
Refresh the form with the latest values from the server. Will trigger
before_load
,
onload
,
refresh
,
timeline_refresh
and
onload_post_render
.
frm.refresh();
frm.save
Trigger form save. Will trigger
validate
,
before_save
,
after_save
,
timeline_refresh
and
refresh
.
It can be used to trigger other save actions like Submit, Cancel and Update. In that case, the relevant events will be triggered.
// save form
frm.save();
// submit form
frm.save('Submit');
// cancel form
frm.save('Cancel');
// update form (after submit)
frm.save('Update');
// all methods returns a promise
frm.enable_save / frm.disable_save
Methods to enable / disable the Save button in the form.
if (frappe.user_roles.includes('Custom Role')) {
frm.enable_save();
} else {
frm.disable_save();
}
frm.email_doc
Open Email dialog for this form.
// open email dialog
frm.email_doc();
// open email dialog with some message
frm.email_doc(`Hello ${frm.doc.customer_name}`);
frm.reload_doc
Reload document with the latest values from the server and call
frm.refresh()
.
frm.reload_doc();
frm.refresh_field
Refresh the field and it's dependencies.
frm.refresh_field('description');
frm.is_dirty
Check if form values has been changed and is not saved yet.
if (frm.is_dirty()) {
frappe.show_alert('Please save form before attaching a file')
}
frm.dirty
Set form as "dirty". This is used to set form as dirty when document values are changed. This triggers the "Not Saved" indicator in the Form Views.
frm.doc.browser_data = navigator.appVersion;
frm.dirty();
frm.save();
Calling save without setting the form dirty will trigger a "No changes in document" toast.
frm.is_new
Check if the form is new and is not saved yet.
// add custom button only if form is not new
if (!frm.is_new()) {
frm.add_custom_button('Click me', () => console.log('Clicked custom button'))
}
frm.set_intro
Set intro text on the top of the form. The function takes two parameters:
message
(string, required) and
color
(string, optional).
Color can be 'blue', 'red', 'orange', 'green' or 'yellow'. Default is blue.
if (!frm.doc.description) {
frm.set_intro('Please set the value of description', 'blue');
}
Intro Text Example
frm.add_custom_button
Add a custom button in the inner toolbar of the page. Alias to page.add_inner_button .
// Custom buttons
frm.add_custom_button('Open Reference form', () => {
frappe.set_route('Form', frm.doc.reference_type, frm.doc.reference_name);
})
// Custom buttons in groups
frm.add_custom_button('Closed', () => {
frm.doc.status = 'Closed'
}, 'Set Status');
frm.change_custom_button_type
Change a specific custom button type by label (and group).
// change type of ungrouped button
frm.change_custom_button_type('Open Reference form', null, 'primary');
// change type of a button in a group
frm.change_custom_button_type('Closed', 'Set Status', 'danger');
frm.remove_custom_button
Remove a specific custom button by label (and group).
// remove custom button
frm.remove_custom_button('Open Reference form');
// remove custom button in a group
frm.remove_custom_button('Closed', 'Set Status');
frm.clear_custom_buttons
Remove all custom buttons from the inner toolbar.
frm.clear_custom_buttons();
frm.set_df_property
Change the docfield property of a field and refresh the field.
// change the fieldtype of description field to Text
frm.set_df_property('description', 'fieldtype', 'Text');
// set the options of the status field to only be [Open, Closed]
frm.set_df_property('status', 'options', ['Open', 'Closed'])
// set a field as mandatory
frm.set_df_property('title', 'reqd', 1)
// set a field as read only
frm.set_df_property('status', 'read_only', 1)
frm.toggle_enable
Toggle a field or list of fields as
read_only
based on a condition.
// set status and priority as read_only
// if user does not have System Manager role
let is_allowed = frappe.user_roles.includes('System Manager');
frm.toggle_enable(['status', 'priority'], is_allowed);
frm.toggle_reqd
Toggle a field or list of fields as mandatory (
reqd
) based on a condition.
// set priority as mandatory
// if status is Open
frm.toggle_reqd('priority', frm.doc.status === 'Open');
frm.toggle_display
Show/hide a field or list of fields based on a condition.
// show priority and due_date field
// if status is Open
frm.toggle_display(['priority', 'due_date'], frm.doc.status === 'Open');
frm.set_query
Apply filters on a Link field to show limited records to choose from. You must call
frm.set_query
very early in the form lifecycle, usually in
setup
or
onload
.
// show only customers whose territory is set to India
frm.set_query('customer', () => {
return {
filters: {
territory: 'India'
}
}
})
// show customers whose territory is any of India, Nepal, Japan
frm.set_query('customer', () => {
return {
filters: {
territory: ['in', ['India', 'Nepal', 'Japan']]
}
}
})
// set filters for Link field item_code in
// items field which is a Child Table
frm.set_query('item_code', 'items', () => {
return {
filters: {
item_group: 'Products'
}
}
})
You can also override the filter method and provide your own custom method on the server side. Just the set the query to the module path of your python method.
// change the filter method by passing a custom method
frm.set_query('fieldname', () => {
return {
query: 'dotted.path.to.custom.custom_query',
filters: {
field1: 'value1'
}
}
})
# python method signature
def custom_query(doctype, txt, searchfield, start, page_len, filters):
# your logic
return filtered_list
frm.add_child
Add a row with values to a Table field.
let row = frm.add_child('items', {
item_code: 'Tennis Racket',
qty: 2
});
frm.refresh_field('items');
frm.call
Call a server side controller method with arguments.
Note: While accessing any server side method using
frm.call()
, you need to whitelist such method using the@frappe.whitelist
decorator.
For the following controller code:
class ToDo(Document):
@frappe.whitelist()
def get_linked_doc(self, throw_if_missing=False):
if not frappe.db.exists(self.reference_type, self.reference_name):
if throw_if_missing:
frappe.throw('Linked document not found')
return frappe.get_doc(self.reference_type, self.reference_name)
You can call it from client using
frm.call
.
frm.call('get_linked_doc', { throw_if_missing: true })
.then(r => {
if (r.message) {
let linked_doc = r.message;
// do something with linked_doc
}
})
frm.trigger
Trigger any form event explicitly.
frappe.ui.form.on('ToDo', {
refresh(frm) {
frm.trigger('set_mandatory_fields');
},
set_mandatory_fields(frm) {
frm.toggle_reqd('priority', frm.doc.status === 'Open');
}
})
frm.get_selected
Get selected rows in Child Tables in an object where key is the table fieldname and values are row names.
let selected = frm.get_selected()
console.log(selected)
// {
// items: ["bbfcb8da6a", "b1f1a43233"]
// taxes: ["036ab9452a"]
// }
frm.ignore_doctypes_on_cancel_all
To avoid cancellation of linked documents during cancel all, you need to set the
frm.ignored_doctypes_on_cancel_all
property with an array of DocTypes of linked documents.
frappe.ui.form.on("DocType 1", {
onload: function(frm) {
// Ignore cancellation for all linked documents of respective DocTypes.
frm.ignore_doctypes_on_cancel_all = ["DocType 2", "DocType 3"];
}
}
In the above example, the system will avoid cancellation for all documents of 'DocType 2' and 'DocType 3' which are linked with document of 'DocType 1' during cancellation.