Nested Form Repeat Parent/Child Index Format

I have a form repeat with a nested form repeat and am having trouble getting the name/index to format correctly, which then makes the data from the child repeat insert into the db as an array based on each index of each child repeat instead of each as their own value on a separate row.

I have scoured every portion of the forums here and the documentation for this and in trying what I have seen to be the most commonly suggested solution for this it is not working as expected, so it is making me think that because most of these topics are 2+ years old there has been a change to how this works within Wappler and so maybe are not relevant anymore?

What I have seen most commonly suggested is in code line for the input(s) of repeat1 add dmx-bind:name"repeat1[{{$index}}]variableName" and of repeat2 “repeat1[{{parentIndex.value}}][repeat2][{{$index}}]variable2Name” (also tried $parent.$index to no avail). When I do this and view in devtools it outputs correctly, like this:

repeat1[0][varName1]:
repeat1[0][varName2]:
repeat1[0][repeat2][0][varName3]: A
repeat1[0][repeat2][0][varName4]: B
repeat1[0][repeat2][1][varName3]: C
repeat1[0][repeat2][1][varName4]: D
repeat1[1][varName1]:
repeat1[1][varName2]:
repeat1[1][repeat2][0][varName3]: E
repeat1[1][repeat2][0][varName4]: F
repeat1[1][repeat2][1][varName3]: G
repeat1[1][repeat2][1][varName4]: H

which should result in the db as:
ID__Ref_ID__Value
1_____0_____A
2_____0_____B
3_____0_____C
4_____0_____D
5_____1_____E
6_____1_____F
7_____1_____G
8_____1_____H

but then the post variables linked to the page form do not generate as expected, and when looking at each variables linked field, each has readded the repeat and index again on the front, resulting in this:

repeat1[0]repeat1[0][varName1]:
repeat1[0]repeat1[0][varName2]:
repeat2[0]repeat1[0][repeat2][0][varName3]: A
repeat2[0]repeat1[0][repeat2][0][varName4]: B
repeat2[1]repeat1[0][repeat2][1][varName3]: C
repeat2[1]repeat1[0][repeat2][1][varName4]: D
repeat1[0]repeat1[1][varName1]:
repeat1[0]repeat1[1][varName2]:
repeat2[0]repeat1[1][repeat2][0][varName3]: E
repeat2[0]repeat1[1][repeat2][0][varName4]: F
repeat2[1]repeat1[1][repeat2][1][varName3]: G
repeat2[1]repeat1[1][repeat2][1][varName4]: H

and instead results in the db as:
ID__Ref_ID__Value
1_____0_____A,E
2_____0_____B,F
3_____0_____C,G
4_____0_____D,H
5_____1_____A,E
6_____1_____B,F
7_____1_____C,G
8_____1_____D,H

I have also tried changing the name and/or id of the repeat 2 where it has its parent index attached to its beginning, which also didn’t work, as it just removes the array brackets and generates a string of the name of whatever you are trying to reference.

Is some way to prevent this from happening, where the repeat id is automatically added to the beginning of the input name in SC, or some other way of achieving this that I am missing? I know I could add a couple hidden inputs and reference each, but based on how frequently I have read this being a common problem, I figure there is probably a more straight forward solution that I am missing.

@Msj1542 - did you ever get any farther with this? I have a use case where I am running into the exact same issue.

I did get it working, but I cant recall exactly what change(s) I made to do so. Let me look at it here in a while, or this evening, and will let you konw!

1 Like

That would be so helpful thank you!!

Hey @Msj1542 - just wanted to follow up to see if you ever saw your method! We wound up editing the source code to accomplish this, in addition to adding a custom class. This is not a very stable change since we edited the source… would love to see if your method is simpler / more stable!

Here is what we updated the dmxFormRepeat.js to be in order to give us back what we needed, in case anyone is curious:

/*!
 Form Repeat
 Version: 1.0.1
 (c) 2022 Wappler.io
 @build 2022-08-18 10:34:49
 */
dmx.Component("form-repeat", {
    initialData: {
        canAdd: !0,
        items: []
    },
    attributes: {
        items: {
            type: [Array, Number],
            default: 0
        },
        min: {
            type: Number,
            default: 0
        },
        max: {
            type: Number,
            default: 1 / 0
        },
        sortable: {
            type: Boolean,
            default: !1
        },
        handle: {
            type: String,
            default: null
        },
        animation: {
            type: Number,
            default: 0
        }
    },
    methods: {
        add: function () {
            this._add()
        },
        remove: function (e) {
            this._remove(e)
        },
        move: function (e, t) {
            this._mode(e, t, !0)
        },
        moveToStart: function (e) {
            this._move(e, 0, !0)
        },
        moveToEnd: function (e) {
            this._move(e, this.children.length - 1, !0)
        },
        moveBefore: function (e) {
            this._move(e, e - 1, !0)
        },
        moveAfter: function (e) {
            this._move(e, e + 1, !0)
        },
        duplicate: function (e) {
            this._duplicate(e)
        },
        reset: function () {
            this._render()
        }
    },
    render: function (e) {
        this._template = this._templateFromChildren(e);

        let pathToRoot = [];
        let currElem = e;
        let closest;

        while (currElem) {

            closest = currElem.closest(".form-repeat");

            if (closest) {

                let currPos = 0;
                let prevSibling = closest.previousElementSibling;

                while (prevSibling) {
                    prevSibling = prevSibling.previousElementSibling;
                    currPos += 1;
                }

                pathToRoot.push([closest.parentNode.id, currPos]);

                currElem = closest.parentNode;
            }

            else {
                currElem = null;
            }

        }

        console.log(pathToRoot);

        const prefix = pathToRoot.length > 0 ? pathToRoot[0][0] + "[" + pathToRoot[0][1] + "]" : ""

        this._template.querySelectorAll("input[name], select[name], textarea[name]").forEach((e => {
            const t = e.name.indexOf("[");

            const name = prefix ? prefix + `[${this.name}]` : this.name;

            t > 0 ? e.setAttribute("dmx-bind:name", name + "[{{$index}}][" + e.name.slice(0, t) + "]" + e.name.slice(t)) : e.setAttribute("dmx-bind:name", name + "[{{$index}}][" + e.name + "]")
        })), this.update({})
    },
    update: function (e, t) {
        dmx.equal(e.items, this.props.items) || this._render(), e.sortable != this.props.sortable && (this.props.sortable ? this.sortable ? this.sortable.option("disabled", !1) : window.Sortable ? this.sortable = Sortable.create(this.$node, {
            handle: this.props.handle,
            onEnd: e => this._move(e.oldIndex, e.newIndex)
        }) : console.warn("Sortable script is missing.") : this.sortable && this.sortable.option("disabled", !0)), e.handle != this.props.handle && this.sortable && this.sortable.option("handle", this.props.handle), e.animation != this.props.animation && this.sortable && this.sortable.option("animation", this.props.animation), dmx.equal(e, this.props) || this._refresh()
    },
    _render: function () {
        const e = dmx.repeatItems("string" == typeof this.props.items ? Number(this.props.items) : this.props.items);
        e.length < this.props.min && (e.length = this.props.min), e.length > this.props.max && (e.length = this.props.max), this._clear();
        for (let t = 0; t < e.length; t++) this._add(e[t])
    },
    _duplicate: function (e) {
        this._add(), this._move(this.children.length - 1, e + 1, !0);
        let t = [];
        this.children[e].$nodes.forEach((e => {
            1 == e.nodeType && t.push(...Array.from(e.querySelectorAll("input[name], textarea[name], select[name]")))
        }));
        let i = [];
        if (this.children[e + 1].$nodes.forEach((e => {
            1 == e.nodeType && i.push(...Array.from(e.querySelectorAll("input[name], textarea[name], select[name]")))
        })), t.length == i.length)
            for (let e = 0; e < t.length; e++) {
                const n = t[e],
                    s = i[e];
                if (n.tagName != s.tagName) break;
                if ("TEXTAREA" == n.tagName) s.value = n.value;
                else if ("INPUT" == n.tagName) {
                    if ("file" == n.type || "password" == n.type) continue;
                    "checkbox" == n.type || "radio" == n.type ? s.checked = n.checked : s.value = n.value
                } else if ("SELECT" == n.tagName)
                    if (n.multiple)
                        for (const e of n.selectedOptions)
                            for (const t of s.options) e.value == t.value && (t.selected = !0);
                    else s.value = n.value;
                s.dispatchEvent(new Event("change"))
            }
    },
    _add: function (e = {}) {
        if (this.children.length >= this.props.max) return;
        const t = new (dmx.Component("repeat-item"))(this._template.cloneNode(!0), this, e);
        t.$nodes.forEach((e => {
            this.$node.appendChild(e), t.$parse(e)
        })), this.children.push(t), this._refresh()
    },
    _remove: function (e) {
        this.children.length <= this.props.min || (this.children.splice(e, 1).forEach((e => {
            e.$destroy()
        })), this._refresh())
    },
    _refresh: function () {
        this.children.forEach(((e, t) => {
            e.set({
                $index: t,
                $canRemove: this.children.length > this.props.min,
                $canMoveToStart: t > 0,
                $canMoveBefore: t > 0,
                $canMoveAfter: t < this.children.length - 1,
                $canMoveToEnd: t < this.children.length - 1
            })
        })), this.set("canAdd", this.children.length < this.props.max), this.set("items", this.children.map((e => e.data)))
    },
    _clear: function () {
        this.children.splice(0).forEach((e => {
            e.$destroy()
        })), this.$node.innerHTML = ""
    },
    _move: function (e, t, i) {
        e < 0 || e >= this.children.length || t < 0 || t >= this.children.length || (this.children.splice(t, 0, this.children.splice(e, 1)[0]), i && (this.$node.innerHTML = "", this.children.forEach((e => {
            e.$nodes.forEach((e => {
                this.$node.appendChild(e)
            }))
        }))), this._refresh())
    },
    _templateFromChildren: function (e) {
        const t = document.createDocumentFragment();
        for (; e.hasChildNodes();) t.appendChild(e.firstChild);
        return t
}
});
//# sourceMappingURL=../maps/dmxFormRepeat.js.map

I have noticed that dmx-form-repeat is automatically translated in array on the payload and auto-handling the $index values…
I accomplished something similar with dmx-repeat instead.
Once you handle your $index manually (as you say) I suggest you giving it a try using dmx-repeat.
(of course I suggest you to keep a back-up before changing your code)

@famousmag so just don’t use form repeat at all, and instead make a repeat region that includes a form?

@Msj1542 just wanted to check in again one more time on this, especially with Wappler 6 coming and the instability of this solution :slight_smile: Any insight you have would be much appreciated!

If I understand your qustion correctly:

The form was already the parent of your form-repeat, right?
So, no more forms inside the dmx-repeat.
Just place your inputs inside the dmx-repeat and give it a try

**(first backup your files in case it doesn’t wok for you).