Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-impl-ui-components && cp -r /tmp/frappe-impl-ui-components/skills/source/impl/frappe-impl-ui-components ~/.claude/skills/frappe-impl-ui-components
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Frappe UI Components & Realtime — Implementation Workflows

Step-by-step workflows for building client-side UI. For form scripting see `frappe-impl-clientscripts`. For server-side API see `frappe-syntax-serverscripts`.

**Version**: v14/v15/v16 | **Note**: v15+ uses Bootstrap 5; Dialog API is stable across all versions.

## Quick Decision: Which UI Component?

```
WHAT do you need?
├── Prompt user for input         → frappe.prompt (simple) or frappe.ui.Dialog (complex)
├── Show a message/alert          → frappe.msgprint / frappe.show_alert / frappe.throw
├── Confirm an action             → frappe.confirm
├── Multi-field data entry popup  → frappe.ui.Dialog with fields
├── Select from a list of records → frappe.ui.form.MultiSelectDialog
├── Full custom page (not a form) → frappe.ui.Page
├── Customize list columns/colors → frappe.listview_settings
├── Visual board for workflow     → Kanban Board (Select field based)
├── Date-based record view        → Calendar View ({doctype}_calendar.js)
├── Hierarchical data display     → Tree View (is_tree DocType)
├── Live updates without refresh  → frappe.publish_realtime + frappe.realtime.on
├── Show background job progress  → frappe.publish_progress
├── Scan barcode/QR code          → frappe.ui.Scanner
└── Custom cell formatting        → formatters in listview_settings or form
```

See `references/decision-tree.md` for the complete decision tree.

## Workflow 1: Dialogs (frappe.ui.Dialog)

### Simple Dialog

```javascript
let d = new frappe.ui.Dialog({
    title: "Enter Details",
    fields: [
        { label: "Full Name", fieldname: "full_name", fieldtype: "Data", reqd: 1 },
        { label: "Email", fieldname: "email", fieldtype: "Data", options: "Email" },
        { label: "Role", fieldname: "role", fieldtype: "Select",
          options: "Developer\nManager\nDesigner" },
    ],
    size: "small",  // "small", "large", or "extra-large"
    primary_action_label: "Create",
    primary_action(values) {
        frappe.call({
            method: "myapp.api.create_user",
            args: values,
            callback(r) {
                if (!r.exc) {
                    frappe.show_alert({ message: "User created", indicator: "green" });
                    d.hide();
                }
            }
        });
    }
});
d.show();
```

**Rule**: ALWAYS call `d.hide()` inside the callback, NEVER before the async call completes.

### Dialog with Table Field

```javascript
let d = new frappe.ui.Dialog({
    title: "Add Items",
    fields: [
        { label: "Customer", fieldname: "customer", fieldtype: "Link",
          options: "Customer", reqd: 1 },
        { fieldtype: "Section Break" },
        { label: "Items", fieldname: "items", fieldtype: "Table",
          in_place_edit: true, reqd: 1,
          fields: [
              { fieldname: "item", label: "Item", fieldtype: "Link",
                options: "Item", in_list_view: 1, reqd: 1 },
              { fieldname: "qty", label: "Qty", fieldtype: "Int",
                in_list_view: 1, default: 1 },
              { fieldname: "rate", label: "Rate", fieldtype: "Currency",
                in_list_view: 1 },
          ],
        },
    ],
    primary_action_label: "Submit",
    primary_action(values) {
        console.log(values);  // { customer: "...", items: [{item, qty, rate}] }
        d.hide();
    }
});
d.show();
```

**Rule**: ALWAYS set `in_list_view: 1` on table child fields you want visible. Fields without it are hidden in the grid.

### Multi-Step Dialog

```javascript
let d = new frappe.ui.Dialog({
    title: "Setup Wizard",
    fields: [
        // Page 1
        { fieldtype: "Section Break", label: "Step 1: Basic Info",
          collapsible: 0 },
        { label: "Name", fieldname: "name", fieldtype: "Data", reqd: 1 },
        // Page 2
        { fieldtype: "Section Break", label: "Step 2: Configuration",
          collapsible: 0 },
        { label: "Option", fieldname: "option", fieldtype: "Select",
          options: "A\nB\nC" },
    ],
    primary_action_label: "Finish",
    primary_action(values) {
        d.hide();
    }
});
d.show();
```

### Key Dialog Methods

| Method | Purpose |
|--------|---------|
| `d.show()` | Display the dialog |
| `d.hide()` | Close the dialog |
| `d.get_values()` | Get all field values as object |
| `d.set_values({field: val})` | Set field values |
| `d.get_field("name")` | Get a specific field control |
| `d.set_df_property("name", "hidden", 1)` | Show/hide fields dynamically |
| `d.disable_primary_action()` | Grey out submit button |
| `d.enable_primary_action()` | Re-enable submit button |

## Workflow 2: Messages & Alerts

### frappe.msgprint: Modal Message

```javascript
// Simple message
frappe.msgprint("Record saved successfully");

// With options
frappe.msgprint({
    title: "Warning",
    message: "This action cannot be undone",
    indicator: "orange",     // green, blue, orange, red
    primary_action: {
        label: "Proceed",
        action() { do_something(); }
    }
});

// List of messages
frappe.msgprint({
    title: "Validation Errors",
    message: "Please fix the following:",
    as_list: true,
    indicator: "red",
});
```

### frappe.throw: Error with Exception

```javascript
// Client-side: shows msgprint and stops execution
frappe.throw("Amount cannot be negative");
```

```python
# Server-side: raises ValidationError, shown as red msgprint
frappe.throw("Amount cannot be negative")
frappe.throw("Not Permitted", frappe.PermissionError)  # specific exception
```

**Rule**: ALWAYS use `frappe.throw` for validation errors. NEVER use `frappe.msgprint` for errors — it does not stop execution.

### frappe.confirm: Yes/No Dialog

```javascript
frappe.confirm(
    "Are you sure you want to delete this record?",
    () => { /* Yes callback */ delete_record(); },
    () => { /* No callback (optional) */ }
);
```

### frappe.prompt: Quick Single-Field Input

```javascript
frappe.prompt(
    { label: "Reason", fieldname: "reason", fieldtype: