Child Table Inside Child Table in Frappe: A Practical Workaround

One of the most common requirements in Frappe/ERPNext projects is to have a child table inside another child table. While this sounds simple, the framework does not support nested child tables out of

 · 2 min read

The Problem

A very common client requirement:

“Can we have a child table inside another child table?”

This usually comes up in use cases like: - Dynamic forms
- Surveys
- Exams (Questions → Options)
- Config-driven UI

However, in Frappe:

❌ Child tables cannot contain another child table
❌ Only one level of table is supported


The Approach

Instead of forcing the framework, we simulate the behavior.

Idea:

  • Store the "grandchild table" as JSON
  • Render it dynamically using frappe.ui.FieldGroup
  • Sync changes back to the JSON field

Data Model

Parent DocType (Exam)

Field Type
subject Data
questions Table (Child)

Child DocType (Questions)

Field Type Description
question Data / Small Text Question text
options_json Code Stores options as JSON

Important: Use Code field, not Data


Grandchild (Stored as JSON)

Example:

[
  {
    "option": "Microsoft",
    "is_correct": 0
  },
  {
    "option": "Netscape",
    "is_correct": 1
  },
  {
    "option": "Google",
    "is_correct": 0
  },
  {
    "option": "Oracle",
    "is_correct": 0
  }
]


JavaScript Implementation

We use form_render to inject a nested table UI inside the child row dialog.

frappe.ui.form.on('Questions', {
    form_render: function (frm, cdt, cdn) {
        let row = locals[cdt][cdn];

        // Access child table grid
        let grid = frm.fields_dict.questions;
        if (!grid || !grid.grid) return;

        // Get open row dialog
        let dialog = grid.grid.open_grid_row;
        if (!dialog) return;

        // Target JSON field wrapper
        let wrapper = dialog.fields_dict.options_json.wrapper;
        wrapper.innerHTML = '';

        // Create dynamic table using FieldGroup
        let field_group = new frappe.ui.FieldGroup({
            fields: [{
                fieldtype: 'Table',
                fieldname: 'options_table',
                label: 'Options',
                in_place_edit: true,
                data: JSON.parse(row.options_json || '[]'),
                fields: [
                    {
                        fieldname: 'option',
                        label: 'Option',
                        fieldtype: 'Data',
                        in_list_view: 1,
                        reqd: 1
                    },
                    {
                        fieldname: 'is_correct',
                        label: 'Correct',
                        fieldtype: 'Check',
                        in_list_view: 1
                    }
                ]
            }],
            body: wrapper
        });

        field_group.make();

        // Sync data back to JSON
        field_group.fields_dict.options_table.grid.wrapper.on('change', () => {
            row.options_json = JSON.stringify(
                field_group.get_value('options_table')
            );
            frm.dirty();
        });
    }
});

Result

This creates:

  • A nested table inside child row dialog
  • Editable options per question
  • Dynamic and flexible UI

Limitations

This is a workaround, not native support.

Keep in mind:

  • ❌ Not suitable for reporting
  • ❌ No DB-level structure for nested data
  • ❌ Requires manual validation

When to Use This

This approach works best for:

  • UI-heavy configurations
  • Dynamic forms
  • Small datasets
  • Non-reporting use cases

Final Thoughts

Instead of fighting the framework, this approach works with it.

It gives:

  • Flexibility
  • Clean UI
  • Quick implementation

Without modifying core behavior.

```


No comments yet.

Add a comment
Ctrl+Enter to add comment