Understanding and Utilizing Form Scripts in Empress

Introduction

Welcome back, developers! Today, we’re diving into the technical depths of Form Scripts in Empress, one of the most crucial features for customizing and enhancing user interaction in your applications. Form Scripts allow you to introduce client-side logic to your forms, enabling automatic fetching of values, adding validation and contextual actions, and more.

Standard Form Scripts

Form Scripts are primarily written in JavaScript. When you create a new DocType, a {doctype}.js file is automatically created where you can write your Form Script.

The syntax is straightforward:

frappe.ui.form.on(doctype, {
    event1() {
        // handle event 1
    },
    event2() {
        // handle event 2
    }
})

Let’s take a look at an example. Assume we have a todo.js file located at frappe/desk/doctype/todo/todo.js. The script might 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 are written in the same file as their parent. The syntax is similar, but you specify the child DocType and handle the event accordingly.

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

Empress also allows you to write Form Scripts by creating Client Script in the system. This comes in handy when the logic is specific to your site. To share Form Scripts across sites, you must include them through Apps.

To create a new Client Script, navigate to:

Home > Customization > Client Script > New

This will open the form to create a new Client Script.

Form Events

Form Scripts rely on events to trigger. Here’s a list of all Form Events that are triggered by the Form. These events will receive 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
    }
})

Here’s a list of events and their descriptions:

Event Name Description
setup Triggered once when the form is created for the first time
before_load Triggered before the form is about to load
onload Triggered when the form is loaded and is about to render
refresh Triggered when the form is loaded and rendered.
onload_post_render Triggered after the form is loaded and rendered
validate Triggered before before_save
before_save Triggered before save is called
after_save Triggered after form is saved
before_submit Triggered before submit is called
on_submit Triggered after form is submitted
before_cancel Triggered before cancel is called
after_cancel Triggered after form is cancelled
timeline_refresh Triggered after form timeline is rendered
{fieldname}_on_form_rendered Triggered when a row is opened as a form in a Table field
{fieldname} Triggered when the value of fieldname is changed

Child Table Events

In the context of a Child Table, additional events are triggered. Besides frm, these events will also receive cdt (Child DocType) and cdn (Child Docname) parameters in their handler functions.

For instance, suppose 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 this table.

// this code is located inside `todo.js`
frappe.ui.form.on('Dynamic Link', { 
    links_add(frm, cdt, cdn) { 
        frappe.msgprint('A row has been added to the links table 🎉 ');
    }
});

Here’s a list of Child Table Events and their descriptions:

Event Name Description
before_{fieldname}_remove Triggered when a row is about to be removed from a Table field
{fieldname}_add Triggered when a row is added to a Table field
{fieldname}_remove Triggered when a row is removed from a Table field
{fieldname}_move Triggered when a row is reordered to another location in a Table field
form_render Triggered when a row is opened as a form in a Table field

Form API

The frm object comes with a list of common methods that you can use in your Form Scripts.

frm.set_value

The frm.set_value method sets 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

The frm.refresh method refreshes the form with the latest values from the server. This will trigger before_load, onload, refresh, timeline_refresh, and onload_post_render events.

frm.refresh();

frm.save

The frm.save method triggers form save. This will trigger validate, before_save, after_save, timeline_refresh, and refresh events. Additionally, it can be used to trigger other save actions like Submit, Cancel, and Update. In those cases, 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

These methods enable or disable the Save button in the form.

if (frappe.user_roles.includes('Custom Role')) {
    frm.enable_save();
} else {
    frm.disable_save();
}

frm.email_doc

This method opens the 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

This method reloads the document with the latest values from the server and calls frm.refresh().

frm.reload_doc();

frm.refresh_field

This method refreshes the field and its dependencies.

frm.refresh_field('description');

frm.is_dirty

This method checks if form values have been changed and are not saved yet.

if (frm.is_dirty()) {
    frappe.show_alert('Please save form before attaching a file')
}

frm.dirty

This method sets the form as “dirty”. This is used to set the 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

This method checks 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

This method sets the 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');
}

frm.add_custom_button

This method adds a custom button in the inner toolbar of the page. It is an 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

This method changes 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

This method removes 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

This method removes all custom buttons from the inner toolbar.

frm.clear_custom_buttons();

frm.set_df_property

This method changes the docfield property of a field and refreshes 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

This method toggles 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

This method toggles 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

This method shows/hides 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

This method applies 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'
        }
    }
})

The python method signature will be as follows:

def custom_query(doctype, txt, searchfield, start, page_len, filters):
    # your logic
    return filtered_list

frm.add_child

This method adds 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

This method calls 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 the 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

This method triggers 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

This method gets selected rows in Child Tables in an object where the 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.

Conclusion

Form Scripts are powerful tools that give you the flexibility to customize and enhance your Empress application to meet your unique needs. With a deep understanding of Form Scripts, you can streamline your application’s behavior and create a more dynamic, interactive user experience. Happy coding!