For years, Frappe apps like ERPNext were built on a desk-based interface powered by the Frappe Framework. It was solid and extensible, but followed a more traditional scripting model that was closely tied to how the framework structured forms and UI interactions.
A few years ago, we started building a new generation of apps like Frappe CRM, with a Vue-based frontend. These brought something we had long wanted: reactivity. Real-time UI updates, a component-driven approach, and a more modern developer experience.
But this new architecture didn’t include the scripting flexibility we had come to rely on in the framework. Features like field-level change triggers, parent-child method calls, and custom logic for dynamic forms were missing.
Rebuilding the sripting layer
The shift to a modern UI wasn’t just about design. It changed how we built features. As Frappe CRM matured, I found myself needing the same scripting power we once took for granted. But now, even simple things like adjusting a field based on user input meant extra workarounds.
These small gaps started adding up. It slowed down development and made the product harder to evolve. Instead of layering on more hacks, it made sense to start fresh with a proper foundation. So I rewrote the scripting layer with a new approach: JavaScript classes that fit naturally into Vue while still offering the control we were used to. The result is a smoother, more powerful way to build dynamic forms.
New in client scripts
Class-based syntax for DocTypes and child tables
Client-side scripting in desk based app's like ERPNext, HRMS etc relies on the older frappe.ui.form.on
pattern:
// Old Syntax
frappe.ui.form.on('CRM Deal', {
category: function (frm) {
if (frm.doc.category === 'A') {
frappe.model.set_value(frm.doctype, frm.docname, 'primary_category', 'C');
} else {
frappe.model.set_value(frm.doctype, frm.docname, 'primary_category', 'D');
}
frm.trigger('primary_category');
}
});
but Frappe CRM is a Vue based app and it needs a better way.
You can now define scripts using modern ES6 classes for both the main Doctype (CRM Deal
) and its child tables (Product Items
). Inside each method, you can:
- Access the document using
this.doc
- Trigger methods with
this.doc.trigger('method')
- Access child table rows using
this.doc.getRow('child_fieldname', idx)
// New Syntax
class CRMDeal {
category() {
this.doc.primary_category = this.doc.category === 'A' ? 'C' : 'D';
this.doc.trigger('primary_category');
}
}
Triggering is now bi-directional (and cleaner)
Interaction | Was It Possible Before? | New Syntax | What’s Better Now |
---|---|---|---|
Child to Parent | ✅ Yes | this.doc.trigger() |
Now easier and scoped to the main doc |
Child to Same Row | ✅ Yes | row.trigger() |
Cleaner and more direct |
Parent to Child | ❌ No | this.doc.getRow(...).trigger() |
Newly supported, adds two-way control |
This means you can now trigger child methods from the parent — a big improvement that unlocks more dynamic behavior across the form.
Global helper functions
Several new helper functions are now available globally in your Client Script:
createDialog
: Create and show a dialog box.toast
: Display success or error messages.socket
: Trigger real-time events.router
: Navigate to different pages using Vue Router.call
: Call an API from the client-side.- App-specific methods like
crm.makePhoneCall
are now available too.
Example using createDialog
:
category() {
let me = this
createDialog({
title: "Update Category",
message: "Update category based on primary category?",
actions: [
{
label: "Update",
variant: "solid",
onClick(close) {
me.doc.category = me.doc.primary_category === "A" ? "C" : "D"
close();
},
},
{
label: "Cancel",
variant: "outline",
onClick(close) {
close();
},
}
]
});
}
Backward compatibility
Worried about breaking changes? Don’t be.
The old Form Script syntax still works, so if you’ve already written custom actions using it, you don’t need to rewrite anything. In fact, you can even mix both styles — use the new class-based syntax for field-level triggers and keep the older one for other logic if that works better for you.
What’s included so far
- On-field change triggers for Doctypes and their child tables
- Ability to trigger child row methods from the parent Doctype
- Clean, bi-directional method triggering between parent and child
What’s coming next
- Custom actions like buttons and bulk actions will soon be supported in this syntax
- List actions will also be added in future versions
- On-refresh triggers are planned to make the class-based experience more complete
If you held off on using Frappe CRM because scripting felt limited, this is a good time to revisit it. With better control and flexibility, customization is back and fits naturally into modern apps. Try it out, and if you build something interesting, share it with us on discuss.frappe.io. We’d love to hear how you're using it.
·
Super cool.