original Source: https://sudhanshubadole.com/blog/development/child-table-inside-child-table-in-frappe-a-practical-workaround
website: https://sudhanshubadole.com/
connect me on: https://www.linkedin.com/in/sudhanshubadole/
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
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)
FieldTypesubjectDataquestionsTable (Child)
Child DocType (Questions)
FieldTypeDescriptionquestionData / Small TextQuestion textoptions_jsonCodeStores 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