Search code examples
javascripthtmlangulartypescriptdataflow

How to get LEVEL 4's select element's values in LEVEL 3's form submit?


The actual complex code is abstracted to make it more readable.

In our Angular 2 project, we have got a component <top-component> (LEVEL 1) like this:

<top-component>
</top-component>

Which has the following template : <some-form> (LEVEL 2):

<some-form>
</some-form>

Which has the following template (LEVEL 3):

<form #f="ngForm" (submit)="handleFormSubmit(f)" >
<input name="Label" value="Label" />
<input name="Value" value="Value" />
<some-select></some-select>
<button> Cancel </button>
<button> Save </button>
</form>

The template of <some-select> (LEVEL 4) is:

<select name="selectData" ngDefaultControl [(ngModel)]="selectData">
      <option *ngFor="let option of options" [ngValue]="option.value">{{option.label}}</option>
</select>

The problem is that when we submit #f="ngForm" to handleFormSubmit(f), the f.value values does not have the values from the some-select's select element.


Solution

  • I fixed this by using a shared service.

    Long story short:

    • Create a component for your form element (select, in this case). (you already have this)
    • Create a shared service between the form's component and the form fields components.
    • Inside your formfield component, on model change, you send the new model to the service.
    • In your form component, you subscribe to any changes on the form fields models and you update all the models.
    • On submit, you will have an object with the models of your form fields components that you updated each thanks to the shared service that was called by each one of the form fields components.

    With this, you can make it work. And you can have a very dynamic form.

    If you need an example tell me and when I can I will make you a simple example here.

    Hope it helped! Cheers!

    Update:

    Let's go from child to parent here.

    SelectField Component:

    @Component({
        selector: 'select-field',
        template: `     
                    <select class="col-md-12 form-control input-xs" name="ibos-newsletter" [(ngModel)]="fieldset.value" (ngModelChange)="onChange($event)">
                        <option *ngFor="let option of options" [ngValue]="option.id"
                                [selected]="option.id === condition">{{option.name}}
                        </option>
                    </select>
        `
    })
    export class SelectField implements OnInit {
        private fieldset = {};
        private options = <any>[];
        private fieldName = 'SelectField';
    
        constructor(private sharedService: SharedService) {
           // we get data from our shared service. Maybe the initial data is gathered and set up in the Form service. 
           // So if the Form service just calls this method we are subscribed and we get the data here
           this.sharedService.updateFieldsetsListener$.subscribe((fieldset) => {
                    if (fieldset.field=== this.fieldName) {
                        this.selectModel = fieldset;
                    }
                }
            );
        }
    
        ngOnInit() {
        }
    
        onChange() {
            // fieldset.value (our model) is automatically updated, because this is binded with (ngModelChange). This means that we are sending an updated fieldset object
            this.sharedService.updateFieldsets(this.fieldset);
        }
    }
    

    SharedService service:

    @Injectable()
    export class SharedService {
        updateFieldsetsListener$: Observable<any>;
    
        private updateFieldsetsSubject = new Subject<any>();
    
        constructor(private jsonApiService: JsonApiService) {
            this.updateFieldsetsListener$ = this.updateFieldsetsSubject .asObservable();
        }
    
        updateFieldsets(fieldset) {
            this.updateFieldsetsSubject .next(fieldset);
        }
    }
    

    Form Component:

    Inside your constructor you should have the subscribe:

    this.sharedService.updateFieldsetsListener$.subscribe((fieldset) => {
            // Here you have a full object of your field set. In this case the select.
            // You can add it to a form object that contains all the fieldsets objects...
            // You can use these formfields objects (instead of sending / receiving only the model) to dynamically: select an option, disable an input, etc...
            }
        );
    

    Personal thoughts:

    • I like this approach because you can manage locally each element of your form. Becomes handy when you have multiselect external libraries, datepickers, etc...
    • I send / receive objects and not just the model because through objects I have the change to decide dynamically de behavior of the element. Imagine you get data from DB in your Form's component, you loop it and invoke the SharedService method to update the fields... They will show up like you tell them to: highlighted, selected, disabled, etc...
    • Eventually, if you render dynamically each element of the form... You will have a form that has the potential to be 100% dynamic and abstract. I managed to make it and now I just have a vector that says which elements of the form have to be rendered and how.