Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested ArrayModel #414

Closed
stekontar opened this issue Jun 22, 2017 · 27 comments
Closed

Nested ArrayModel #414

stekontar opened this issue Jun 22, 2017 · 27 comments

Comments

@stekontar
Copy link

stekontar commented Jun 22, 2017

I am trying to create nested arrayModel like the following example
https://plnkr.co/edit/vSODkb8i7o54X3fST9VF?p=preview
but it seems that currently is not supported (or I am doing something wrong)
I already check issue #225 and #289 (and I wrote another comment there) but I cannot figure out how to do it.

Suggestion: in dynamic-form.service.ts in method findById I add the following snippet in order to get the nested model

 if (controlModel instanceof DynamicFormArrayModel) {
       for (let nestControlModel of controlModel.groups){
           findByIdFn(id, (nestControlModel as DynamicFormArrayGroupModel).group);
       }
 }

currently I face a problem in order to get from FormGroup the nested FormArray based on context.id
Ideally I would like support of n-th level of modelArray
Thank you.

Regards,
S.

@udos86
Copy link
Owner

udos86 commented Jun 29, 2017

@stekontar Hi!

I'd never thought someone would ever goes this far :-)

Could you provide your form model?

My first guess is that it actually should work...

@udos86 udos86 added the core label Jun 29, 2017
@stekontar
Copy link
Author

stekontar commented Jun 29, 2017

Hi @udos86

I deal with huge forms (almost 100 fields) and I must say that your library saved me a lot of time.
imagine an object with a lot of nested objects. all kind of fields and a lot of validations. :(
So, my first approach worked well with the static model but when I tried to reproduce it with
formService.addFormGroupControl() like bootstrapExample did not go as expected

kendo-example.model.ts

export const DYNAMIC_NESTED_GROUP: DynamicFormGroupModel[] = [
    new DynamicFormGroupModel({
        id: "FormGroup_level_1",
        legend: "Form Group Level 1",
        group: [
            new DynamicInputModel({
                id: "level_1_Input_1",
                label: "Level 1 Input 1"
            }),
            new DynamicInputModel({
                id: "level_1_Input_2",
                label: "Level 1 Input 2"
            }),
            new DynamicFormGroupModel({
                id: "FormGroupLevel2",
                legend: "Form Group Level 2",
                group: [
                    new DynamicInputModel({
                        id: "level_2_Input_1",
                        label: "Level 2 Input 1"
                    }),
                    new DynamicInputModel({
                        id: "level_2_Input_2",
                        label: "Level 2 Input 2"
                    }),
                    new DynamicFormGroupModel({
                        id: "FormGroupLevel3",
                        legend: "Form Group Level 3",
                        group: [
                            new DynamicInputModel({
                                id: "level_3_Input_1",
                                label: "Level 3 Input 1"
                            }),
                            new DynamicInputModel({
                                id: "level_3_Input_2",
                                label: "Level 3 Input 2"
                            }),
                        ]
                    })
                ]
            })
        ]
    })
]

I was glad but soon the next step unfortunately did not worked


export const DYNAMIC_NESTED_ARRAY: DynamicFormGroupModel[] = [

    new DynamicFormGroupModel({
        id: "FormGroup_level_1",
        legend: "Form Group Level 1",
        group: [
            new DynamicFormArrayModel(
                {
                    id: "FormArray_Level_1",
                    initialCount: 1,
                    label: "Form Array Level 1",
                    createGroup: () => {
                        return [
                            new DynamicInputModel({
                                id: "level_1_Input_1",
                                label: "Level 1 Input 1"
                            }),
                            new DynamicInputModel({
                                id: "level_1_Input_2",
                                label: "Level 1 Input 2"
                            }),

                            new DynamicFormArrayModel({
                                id: "FormArray_Level_2",
                                initialCount: 1,
                                label: "Form Array Level 2",
                                createGroup: () => {
                                    return [
                                        new DynamicInputModel({
                                            id: "level_2_Input_1",
                                            label: "Level 2 Input 1"
                                        }),
                                        new DynamicInputModel({
                                            id: "level_2_Input_2",
                                            label: "Level 2 Input 2"
                                        }),

                                        new DynamicFormArrayModel({
                                            id: "FormArray_Level_3",
                                            initialCount: 1,
                                            label: "Form Array Level 3",
                                            createGroup: () => {
                                                return [
                                                    new DynamicInputModel({
                                                        id: "level_3_Input_1",
                                                        label: "Level 3 Input 1"
                                                    }),
                                                    new DynamicInputModel({
                                                        id: "level_3_Input_2",
                                                        label: "Level 3 Input 2"
                                                    }),
                                                ]
                                            }
                                        }),
                                    ]
                                }
                            }),
                        ];
                    }
                }
            )
        ]
    })

]

kendo-example.component.html

  <ng-template modelType="ARRAY" let-context="context" let-index="index">
                <span class="k-icon k-i-plus" (click)="insert(context, index)"></span>
                <span class="k-icon k-i-minus" (click)="remove(context, index)"></span>
 </ng-template>

kendo-example.component.ts


    insert(context: DynamicFormArrayModel, index: number) {
        let control = this.formGroup.get("FormGroup_level_1").get(context.id) as FormArray;
        this.formService.insertFormArrayGroup(index, control, context);
    }

    remove(context: DynamicFormArrayModel, index: number) {
        let control = this.formGroup.get("FormGroup_level_1").get(context.id) as FormArray;
        this.formService.removeFormArrayGroup(index, control, context);
    }

I also tried to use the formService in order to add it dynamically (that's the main goal) but did not worked either

Thank you very much.
Regards,
S.

@renatoaraujoc
Copy link

I had to comment on this, 100+ fields? Are you launching a missile bro? Haha

@udos86
Copy link
Owner

udos86 commented Jul 5, 2017

@stekontar

From a usability point of view I'd not recommend nesting such an enormous amount of controls in one form.

However, I made some quick tests with nested form arrays and I could not verify any unexpected behavior here.

What do you exactly mean by "did not go as expected" and "the next step unfortunately did not worked"?

@bappy004
Copy link

bappy004 commented Jul 5, 2017

@udos86, I reckon @stekontar meant the Add-Remove buttons didnt work and he made various attempts but they weren't working? I've had the same problem using the input data mentioned above. Any suggestions?
Thanks.

@stekontar
Copy link
Author

stekontar commented Jul 6, 2017

@bappy004 indeed. the problem is on add/remove buttons. specifically the control is undefined as the angular function this.formGroup.get(context.id) only works for the first level.
in case of nested Form Groups if you do something like this.formGroup.get('FormGroup_level_1.FormGroup_level_2.FormGroup_level_3'+context.id) the correct control would be returned (meaning that angular FormGroup.get() function requires the full path)
BUT
in case of nested Form Arrays in order to find the control we must specify the index of array of the parent control.
something like
this.formGroup.get('FormGroup_level_1.0.FormGroup_level_2.1.FormGroup_level_3'+context.id)
assume that the control is triggerd in first index of array 1 and in second index of array two.

As first approach I m trying to set a "parentGroup"/"parentArray" in DynamicFormGroupModel/DynamicFormArrayModel property (with current index if it is array) in order to know each control's "path"

that's what I have figure out until now. I don't know if it is in the right way, still looking.
Regards,
S.

@stekontar
Copy link
Author

@udos86
I know that those kind of forms are not convenient but I could not avoid it.

"What do you exactly mean by "did not go as expected" and "the next step unfortunately did not worked"?"

About "did not go as expected" I was referencing to DynamicFormGroupModel but please ignore it, it was my mistake.
About "the next step unfortunately did not worked" I am referencing to DynamicFormArrayModel and its functionality of insert/move/delete that it is not working.

Thank you.
S.

@udos86
Copy link
Owner

udos86 commented Jul 6, 2017

@stekontar Now I see, thanks!

Let me think about it. I'll try to provide a solution.

@ghost
Copy link

ghost commented Jul 7, 2017

The problem is when there are FormArrays nested in FormArrays. Heres my suggestion from the duplicate ticket.

Since the every FormGroup control is available to the formService.createFormGroup when it first creates the formGroup instance, while traversing down the model, maybe it will make sense to inject the control to its corresponding model instance, such as DynamicFormArrayModel, DynamicFormGroupModel, DynamicInputModel, etc?

So you can simply do something like this:

removeItem(index: number, context: DynamicFormArrayModel) {
  this.formService.removeFormArrayGroup(index, context.control, context);
}   

@udos86
Copy link
Owner

udos86 commented Jul 7, 2017

@kuwas

Thanks for your suggestion but a component should never be referenced by a model.

@stekontar @bappy004

I figured out a solution to this (see 63ac803).

From 1.4.18 on, a DynamicFormArrayGroupModel will have a parent property which references null or an ancestor DynamicFormArrayGroupModel.

@ghost
Copy link

ghost commented Jul 7, 2017

@udos86

How do you envision calculating the entire formGroup.get string from a deeply nested addItem beside say an input, such as Group > Array > Array > Array > Input?

@udos86
Copy link
Owner

udos86 commented Jul 7, 2017

@kuwas

DynamicFormArrayGroupModel has already a property context that references the DynamicFormArrayModel from which you can read the form control id.

Then just traverse up by using the new parent property.

@ghost
Copy link

ghost commented Jul 7, 2017

Is there no way to just make the corresponding control available in the ng-template to send to the action handler? Not being available in the model, fine, but the view? Traversing up every parent just to figure out its corresponding control seems an overly complex solution to a very simple problem imo.

@udos86
Copy link
Owner

udos86 commented Jul 7, 2017

@kuwas

Is there no way to just make the corresponding control available in the ng-template to send to the action handler? Not being available in the model, fine, but the view?

No, because when applying a template to a form array NgTemplateOutletContext is bound to DynamicFormArrayGroupModel.

Traversing up every parent just to figure out its corresponding control seems an overly complex solution to a very simple problem imo.

Most developers will never face a use case of several levels of form arrays.

At least as long as they're not suffering of overly complex form UI designs.

Besides, I don't think a simple while-loop is that complicated for creating the form group path:

I'll also add a getter path to DynamicFormArrayGroupModel which will give the full form array path traversing up a nested form array structure.

@stekontar
Copy link
Author

@udos86
thank you for your interest and the quick response on this issue.
I update the code from development branch and try to make it work.

using the following insert snippet, seems that path is not updated correctly.
using the above schema if you try to
add new FormArray_Level_1 and after that a FormArray_Level_2 (in latest FormArray_Level_1) the path of control should be [FormArray_Level_1],[1] instead I m getting
[FormArray_Level_2],[0]
same thing on third level
if you try to add new FormArray_Level_2 and after that a FormArray_Level_3 (in latest FormArray_Level_2) the path of control should be
[FormArray_Level_1],[0],[FormArray_Level_2],[1]
instead I m getting [FormArray_Level_3],[0] without parent object

am I missing something?


insert(context: DynamicFormArrayModel, index: number, group: DynamicFormControlModel) {
   
    let grp = context.groups[0] as DynamicFormArrayGroupModel;
    let p = "FormGroup_level_1." + grp.path.join(".").slice(0,-2);
    this.arrayControl = this.formGroup.get(p) as FormArray;
    this.formService.insertFormArrayGroup(index, this.arrayControl, context);
  }

Regards,
S.

@udos86
Copy link
Owner

udos86 commented Jul 9, 2017

@stekontar

am I missing something?

This is, what you would like to do:


insert(context: DynamicFormArrayModel, index: number){ 
    
    let group = context.get(index);
    let controlPath = group.path.pop();
    let control = this.formGroup.get(controlPath) as FormArray;

    this.formService.insertFormArrayGroup(index, control, context);
}

@udos86 udos86 closed this as completed in 37fa5a9 Jul 9, 2017
@stekontar
Copy link
Author

stekontar commented Jul 10, 2017

@udos86

Thank's for your answer.
Probably I am doing something wrong as neither your snippet is working on bootstrap-example.
I will check it again and let you know.

Regards,
S.

@ghost
Copy link

ghost commented Jul 13, 2017

@udos86

Thanks, the new solution is great.

@stekontar
Copy link
Author

stekontar commented Jul 13, 2017

@kuwas @udos86

The solution is very nice but I thing that there is a bug.
While you add new nested arrays its parent property is not setting properly.
(taking the above schema as example)
Could you please verify that this bug exists by adding/removing as many first/second/third level of arrays?
(using default kendo/bootstrap example)

Regards,
S.

@ghost
Copy link

ghost commented Jul 13, 2017

@stekontar @udos86

Yes, I gave this a try and found an issue as well. Basically all is well if the path is Array > Array > Array > Input, the path array gets populated correctly. However, if you start putting groups in between, the chain is broken and theres no way to traverse up the tree. Group > Array > Group > Array > Input is what I tried, the path only populates up to the closest array that isnt broken in the chain.

Perhaps we should open a new issue?

@stekontar
Copy link
Author

@kuwas
problem is that
if you add an new first level array
and then try to add a new second level array in last crated first level an exception is thrown.
I found that during DynamicFormArrayModel.insertGroup the parent is not being initialized
but at that moment parent is not available.

@udos86
Copy link
Owner

udos86 commented Jul 17, 2017

@stekontar @kuwas

The current solution only works for directly nested form arrays. Otherwise I would have to introduce a parent property for DynamicFormControlModel in general.

Thanks for reporting the bug. I'll try to deliver a fix as soon as possible!

@udos86 udos86 reopened this Jul 17, 2017
@udos86
Copy link
Owner

udos86 commented Jul 17, 2017

@stekontar @kuwas

There'll be a new approach in 1.4.19 to solve this globally once and for all.

Now whenever a form group is created via DynamicFormService a parent property is set for every DynamicFormControlModel.

You can now retrieve a full form control path by calling getPath() on DynamicFormService and passing either a DynamicFormControlModel or a DynamicFormArrayGroupModel.

@ghost
Copy link

ghost commented Jul 17, 2017

@udos86

Great! Thanks for the fix.

@udos86 udos86 closed this as completed in 380a651 Jul 17, 2017
@stekontar
Copy link
Author

@udos86 @udos86
I appreciate your interest about this issue.
the new fix is quite helpful but still has a bug. As I described above the problem is that during insert new formarray the parent is not being initialized correctly.

I believe that problem would be solved if in dynamic-form.service.ts replace the function insertFormArrayGroup
from

insertFormArrayGroup(index: number, formArray: FormArray, model: DynamicFormArrayModel): void {
       formArray.insert(index, this.createFormGroup(model.insertGroup(index).group));
}

to this

 insertFormArrayGroup(index: number, formArray: FormArray, model: DynamicFormArrayModel): void {
        let newModel = model.insertGroup(index);
        formArray.insert(index, this.createFormGroup(newModel.group, null, newModel));
}

now I can call the function from bootstrap-example.component.ts with this.

insert(context: DynamicFormArrayModel, index: number) {
       let group = context.get(0);
       let controlPath = this.formService.getPath(group).join(".").slice(0, -2);
       console.log(controlPath)
       let control = this.formGroup.get(controlPath) as FormArray;
       this.formService.insertFormArrayGroup(index, control, context);
}

currently I test it with 3 level deep arrays (the model at the top) and is working correctly.
I can add as many arrays as I want in any level.
I still have to do a lot of tests, I let you know if something comes up.

Regards,
S.

@udos86
Copy link
Owner

udos86 commented Jul 18, 2017

@stekontar Thanks for your tests.

I actually thought this problem would be ultimately solved by the current implementation but I have to check it again.

By the way: You don't need to call .join(".").slice(0, -2) on the form path as the get() method of FormGroup accepts a path array.

Please also note that you can now directly reference a DynamicFormArrayGroupModel via a local default template variable as it internally sets $implicit property.

@udos86 udos86 reopened this Jul 18, 2017
@udos86
Copy link
Owner

udos86 commented Jul 18, 2017

@stekontar I can confirm that your line of code is the missing piece to make it all work finally. Thanks!

@udos86 udos86 closed this as completed in 32e428e Jul 18, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants