client-scripts

SKILL.md

Frappe Client Scripts Reference

Complete reference for client-side JavaScript development in Frappe Framework.

When to Use This Skill

  • Writing form scripts (refresh, validate, field events)
  • Manipulating form fields (show/hide, require, read-only)
  • Creating dialogs and prompts
  • Making API calls from client
  • Customizing list views
  • Adding custom buttons
  • Handling child table events

Form Script Location

my_app/
└── my_module/
    └── doctype/
        └── my_doctype/
            └── my_doctype.js    # Client script

Form Events

Complete Event Reference

frappe.ui.form.on('My DocType', {
    // === LOAD EVENTS ===

    setup: function(frm) {
        // Called once when form is created (before data loads)
        // Use for: setting queries, initializing variables
        frm.set_query('customer', () => ({ filters: { status: 'Active' } }));
    },

    onload: function(frm) {
        // Called when form data is loaded (before refresh)
        // Use for: setting defaults for new docs
        if (frm.is_new()) {
            frm.set_value('posting_date', frappe.datetime.nowdate());
        }
    },

    onload_post_render: function(frm) {
        // Called after form is rendered
        // Use for: DOM manipulation, focus setting
        frm.get_field('customer').focus();
    },

    refresh: function(frm) {
        // Called every time form refreshes
        // Use for: custom buttons, field toggles, indicators
        if (!frm.is_new()) {
            frm.add_custom_button(__('Action'), () => do_action(frm));
        }
        frm.toggle_display('section_name', frm.doc.show_section);
    },

    // === SAVE EVENTS ===

    validate: function(frm) {
        // Called before save - return false to prevent
        if (frm.doc.end_date < frm.doc.start_date) {
            frappe.msgprint(__('End Date cannot be before Start Date'));
            return false;
        }
    },

    before_save: function(frm) {
        // Called after validate, before server request
        frm.doc.last_updated_by = frappe.session.user;
    },

    after_save: function(frm) {
        // Called after successful save
        frappe.show_alert({
            message: __('Saved successfully'),
            indicator: 'green'
        });
    },

    // === WORKFLOW EVENTS ===

    before_submit: function(frm) {
        // Called before document submission
    },

    on_submit: function(frm) {
        // Called after successful submission
    },

    before_cancel: function(frm) {
        // Called before cancellation
    },

    after_cancel: function(frm) {
        // Called after cancellation
    },

    // === FIELD EVENTS ===

    customer: function(frm) {
        // Called when 'customer' field changes
        if (frm.doc.customer) {
            fetch_customer_details(frm);
        }
    },

    posting_date: function(frm) {
        // Called when 'posting_date' field changes
        calculate_due_date(frm);
    }
});

Field Manipulation

Display Properties

// Show/hide field
frm.toggle_display('fieldname', true);  // Show
frm.toggle_display('fieldname', false); // Hide
frm.toggle_display(['field1', 'field2'], condition);

// Set read-only
frm.set_df_property('fieldname', 'read_only', 1);
frm.toggle_enable('fieldname', false);  // Disable

// Set required
frm.set_df_property('fieldname', 'reqd', 1);
frm.toggle_reqd('fieldname', true);
frm.toggle_reqd(['field1', 'field2'], condition);

// Set hidden
frm.set_df_property('fieldname', 'hidden', 1);

// Change label
frm.set_df_property('fieldname', 'label', 'New Label');

// Change description
frm.set_df_property('fieldname', 'description', 'Help text');

// Change options (for Select)
frm.set_df_property('fieldname', 'options', 'Option1\nOption2\nOption3');

// Refresh after changes
frm.refresh_field('fieldname');
frm.refresh_fields();

Set Values

// Set single value
frm.set_value('fieldname', value);

// Set multiple values
frm.set_value({
    'field1': 'value1',
    'field2': 'value2',
    'field3': 'value3'
});

// Set with callback
frm.set_value('fieldname', value).then(() => {
    // After value is set
});

// Clear field
frm.set_value('fieldname', null);
frm.set_value('fieldname', '');

// Set default value
frm.set_df_property('fieldname', 'default', 'default_value');

Link Field Queries

// Basic filter
frm.set_query('customer', function() {
    return {
        filters: {
            status: 'Active',
            customer_type: 'Company'
        }
    };
});

// Dynamic filter based on form values
frm.set_query('item_code', function() {
    return {
        filters: {
            item_group: frm.doc.item_group,
            is_stock_item: 1
        }
    };
});

// Filter in child table
frm.set_query('item_code', 'items', function(doc, cdt, cdn) {
    let row = locals[cdt][cdn];
    return {
        filters: {
            warehouse: row.warehouse || doc.default_warehouse
        }
    };
});

// Custom query (server method)
frm.set_query('supplier', function() {
    return {
        query: 'my_app.api.get_suppliers',
        filters: {
            region: frm.doc.region
        }
    };
});

// Clear query
frm.set_query('fieldname', null);

Custom Buttons

refresh: function(frm) {
    // Simple button
    frm.add_custom_button(__('Do Something'), function() {
        do_something(frm);
    });

    // Button in group/dropdown
    frm.add_custom_button(__('Action 1'), function() {
        action_1(frm);
    }, __('Actions'));

    frm.add_custom_button(__('Action 2'), function() {
        action_2(frm);
    }, __('Actions'));

    // Primary button (highlighted)
    frm.add_custom_button(__('Submit'), function() {
        submit_doc(frm);
    }).addClass('btn-primary');

    // Button with icon
    let btn = frm.add_custom_button(__('Print'), function() {
        print_doc(frm);
    });
    btn.prepend('<i class="fa fa-print"></i> ');

    // Conditional buttons
    if (frm.doc.status === 'Draft') {
        frm.add_custom_button(__('Submit for Review'), function() {
            submit_for_review(frm);
        });
    }

    // Remove button
    frm.remove_custom_button(__('Do Something'));
    frm.remove_custom_button(__('Action 1'), __('Actions'));

    // Clear all buttons
    frm.clear_custom_buttons();

    // Page actions
    frm.page.set_primary_action(__('Save'), function() {
        frm.save();
    });

    frm.page.set_secondary_action(__('Cancel'), function() {
        frappe.set_route('List', 'My DocType');
    });
}

Child Table Operations

Events

frappe.ui.form.on('My DocType Item', {
    // Row added
    items_add: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        row.warehouse = frm.doc.default_warehouse;
        frm.refresh_field('items');
    },

    // Before row removed (can prevent)
    before_items_remove: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        if (row.is_mandatory) {
            frappe.throw(__('Cannot remove mandatory item'));
        }
    },

    // Row removed
    items_remove: function(frm, cdt, cdn) {
        calculate_total(frm);
    },

    // Field in row changes
    qty: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        row.amount = flt(row.qty) * flt(row.rate);
        frm.refresh_field('items');
        calculate_total(frm);
    },

    rate: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        row.amount = flt(row.qty) * flt(row.rate);
        frm.refresh_field('items');
        calculate_total(frm);
    },

    item_code: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        if (row.item_code) {
            frappe.call({
                method: 'my_app.api.get_item_details',
                args: { item_code: row.item_code },
                callback: function(r) {
                    if (r.message) {
                        frappe.model.set_value(cdt, cdn, {
                            'rate': r.message.rate,
                            'uom': r.message.uom,
                            'description': r.message.description
                        });
                    }
                }
            });
        }
    }
});

function calculate_total(frm) {
    let total = 0;
    frm.doc.items.forEach(item => {
        total += flt(item.amount);
    });
    frm.set_value('total', total);
}

Manipulating Rows

// Add row
let row = frm.add_child('items', {
    item_code: 'ITEM-001',
    qty: 10,
    rate: 100
});
frm.refresh_field('items');

// Get row by index
let first_row = frm.doc.items[0];

// Get row by name
let row = locals['My DocType Item'][cdn];

// Update row
frappe.model.set_value(cdt, cdn, 'fieldname', value);
frappe.model.set_value(cdt, cdn, {
    'field1': 'value1',
    'field2': 'value2'
});

// Remove row
frm.get_field('items').grid.grid_rows[0].remove();
frm.refresh_field('items');

// Remove all rows
frm.clear_table('items');
frm.refresh_field('items');

// Iterate rows
frm.doc.items.forEach((item, idx) => {
    console.log(idx, item.item_code);
});

Dialogs

Simple Prompt

// Single field
frappe.prompt(
    {
        fieldname: 'reason',
        fieldtype: 'Small Text',
        label: 'Reason',
        reqd: 1
    },
    function(values) {
        console.log(values.reason);
    },
    __('Enter Reason'),
    __('Submit')
);

Multi-field Prompt

frappe.prompt([
    {
        fieldname: 'customer',
        fieldtype: 'Link',
        options: 'Customer',
        label: 'Customer',
        reqd: 1
    },
    {
        fieldname: 'date',
        fieldtype: 'Date',
        label: 'Date',
        default: frappe.datetime.nowdate()
    },
    {
        fieldname: 'priority',
        fieldtype: 'Select',
        label: 'Priority',
        options: 'Low\nMedium\nHigh',
        default: 'Medium'
    }
], function(values) {
    process_data(values);
}, __('Enter Details'), __('Process'));

Custom Dialog

let dialog = new frappe.ui.Dialog({
    title: __('Custom Dialog'),
    fields: [
        {
            fieldname: 'customer',
            fieldtype: 'Link',
            options: 'Customer',
            label: __('Customer'),
            reqd: 1,
            get_query: function() {
                return { filters: { status: 'Active' } };
            },
            change: function() {
                // Field change handler
                let value = dialog.get_value('customer');
                if (value) {
                    dialog.set_value('customer_name', 'Loading...');
                }
            }
        },
        { fieldtype: 'Column Break' },
        {
            fieldname: 'customer_name',
            fieldtype: 'Data',
            label: __('Customer Name'),
            read_only: 1
        },
        { fieldtype: 'Section Break', label: 'Items' },
        {
            fieldname: 'items',
            fieldtype: 'Table',
            label: __('Items'),
            cannot_add_rows: false,
            in_place_edit: true,
            fields: [
                {
                    fieldname: 'item',
                    fieldtype: 'Link',
                    options: 'Item',
                    in_list_view: 1,
                    label: __('Item')
                },
                {
                    fieldname: 'qty',
                    fieldtype: 'Float',
                    in_list_view: 1,
                    label: __('Qty')
                }
            ]
        }
    ],
    size: 'large', // small, large, extra-large
    primary_action_label: __('Submit'),
    primary_action: function(values) {
        console.log(values);
        dialog.hide();
        process_dialog(values);
    },
    secondary_action_label: __('Cancel')
});

dialog.show();

// Set values
dialog.set_value('customer', 'CUST-001');
dialog.set_values({
    'customer': 'CUST-001',
    'date': frappe.datetime.nowdate()
});

// Get values
let values = dialog.get_values();
let customer = dialog.get_value('customer');

// Access fields
let field = dialog.get_field('customer');
field.set_description('Select active customer');

Confirmation Dialog

frappe.confirm(
    __('Are you sure you want to delete this?'),
    function() {
        // On Yes
        delete_record();
    },
    function() {
        // On No (optional)
    }
);

API Calls

frappe.call

// Basic call
frappe.call({
    method: 'my_app.api.get_data',
    args: {
        customer: frm.doc.customer
    },
    callback: function(r) {
        if (r.message) {
            frm.set_value('data', r.message);
        }
    }
});

// With loading indicator
frappe.call({
    method: 'my_app.api.process',
    args: { data: frm.doc },
    freeze: true,
    freeze_message: __('Processing...'),
    callback: function(r) {
        frappe.msgprint(__('Done!'));
    },
    error: function(r) {
        frappe.msgprint(__('Error occurred'));
    }
});

// Async/await
async function getData() {
    const r = await frappe.call({
        method: 'my_app.api.get_data',
        args: { id: 123 }
    });
    return r.message;
}

// Promise chain
frappe.call({
    method: 'my_app.api.get_data'
}).then(r => {
    return frappe.call({
        method: 'my_app.api.process',
        args: { data: r.message }
    });
}).then(r => {
    console.log('Done', r.message);
});

Messages & Alerts

// Toast alert
frappe.show_alert({
    message: __('Success!'),
    indicator: 'green'  // green, blue, orange, red
}, 5);  // seconds

// Message dialog
frappe.msgprint({
    title: __('Information'),
    message: __('This is important'),
    indicator: 'blue'
});

// Error (stops execution)
frappe.throw(__('Cannot proceed'));

// Confirmation required
frappe.validated = false;  // In validate event

Utilities

// Date/Time
frappe.datetime.nowdate();           // "2024-01-15"
frappe.datetime.now_datetime();      // "2024-01-15 10:30:00"
frappe.datetime.add_days("2024-01-15", 7);
frappe.datetime.add_months("2024-01-15", 1);

// Formatting
frappe.format(1234.56, {fieldtype: 'Currency'});
format_currency(1234.56, 'USD');
flt(value);  // Float
cint(value); // Integer

// Navigation
frappe.set_route('Form', 'Customer', 'CUST-001');
frappe.set_route('List', 'Customer');
frappe.new_doc('Customer');

// Translation
__('Translate this');
__('Hello {0}', [name]);
Weekly Installs
7
GitHub Stars
9
First Seen
Feb 5, 2026
Installed on
opencode7
claude-code7
codex7
github-copilot6
kimi-cli6
amp6