Search code examples
angulartypescriptangular-reactive-formsangular-forms

How to build the Nested Reactive Form structure using angular based on the JSON Data provided


I'm new to angular. Below is the JSON data I'm getting from GET call in postman. I'm trying to build the form using this

  data = {
    headline: [
      {
        language: 'en',
        headlineText: 'example headline',
      },
    ],
    bodyText: [
      {
        language: 'en',
        bodyText: 'example bodytext',
      },
    ],
    location: {
      name: 'mkontheway',
      openingHours: [
        {
          day: 'Mon-frd',
          timing: '10.00PM-9AM',
        },
      ],
      address: {
        postCode: 'test',
        country: 'test',
      },
    },
  };

But here Day and Timing fields are repeating 2 times. I'm not able to get the control of these two fields correctly because of nested form structure. Please someone suggest me the solution to build a complex nested form structure in angular.

Thanks in advance.

Here is the stack blitz link

Angular Nested Form

Here there are nested form groups and form arrays are there. Please suggest me how to achieve this nested form implementation using angular reactive forms

I took the reference from this stack blitz example Reference Stack blitz


Solution

  • You only need create an empty formArray in openingHours in the location

       this.createAppForm = this.fb.group({
          ...
          location: this.fb.group({
            name: ['', Validators.required],
            openingHours: this.fb.array([]), //<--this is an "empty" formAray
            address: this.fb.group({
               ...
            }),
          }),
        });
    

    But (always there're a but) seeing your code you has many repetitions to create the differents FormGroups. And repeat code makes we enter in trouble early.

    So we are going to create functions that return the formGroups that compouned your FormGroup

    Imagine you has some like

      setHeadLine(data: any = null) {
        data = data || { language: null, headlineText: null };
        return this.fb.group({
          headlineText: data.headlineText,
          language: data.language,
        });
      }
    
      setBodyText(data: any = null) {
        data = data || { bodyText: null, language: null };
        return this.fb.group({
          bodyText: data.bodyText,
          language: data.language,
        });
      }
    
      setAddress(data: any = null) {
        data = data || { postCode: null, country: null };
        return this.fb.group({
          postCode: [data.postCode, Validators.required],
          country: [data.country, Validators.required],
        });
      }
    
      setOpeningHour(data: any = null) {
        data = data || { day: null, timing: null };
        return this.fb.group({
          day: [data.day, Validators.required],
          timing: [data.timing, Validators.required],
        });
      }
    
      setLocation(data: any = null) {
        console.log(data);
        data = data || { name: null, openingHours: null, address: null };
        return this.fb.group({
          name: [data.name, Validators.required],
          openingHours: this.fb.array(
            data.openingHours
              ? data.openingHours.map((x) => this.setOpeningHour(x))
              : []
          ),
          address: this.setAddress(data.address),
        });
      }
    

    See how the "setLocation" call to the function setAddress to create the formGroup adress and how openingHours is a this.fb.array. If data.openingHours is an array, convert the array of objects in an array of formGroups and create the formArray, else return an empty array

    Finally you create a function that return your formGroup

      setFormGroup(data: any = null) {
        data = data || { headline: null, bodyText: null, location: null };
        return this.fb.group({
          headline: this.fb.array(
            data.headline ? data.headline.map((x) => this.setHeadLine(x)) : []
          ),
          bodyText: this.fb.array(
            data.bodyText ? data.bodyText.map((x) => this.setBodyText(x)) : []
          ),
          location: this.setLocation(data.location),
        });
      }
    

    I this way you not repeat code. In the constructor you can write

     this.createAppForm = this.setFormGroup(this.data);
    

    And when you add a FormGroup simply call the functions:

      addHeadline() {
        this.getHeadlineFormData().push(this.setHeadLine())
      }
    
      addBodyText() {
        this.getBodyTextFormData().push(this.setBodyText())
      }
    
      addOpeningHours() {
        this.getopeningHoursFormData().push(this.setOpeningHour())
      }
    

    Well, (this last is only a suggestion) I think that the code becomes more clear using "getter", so if you replace

    //replace
      getHeadlineFormData() {
        return <FormArray>this.createAppForm.get('headline');
      }
    //by 
      get headlineFormData() {
        return <FormArray>this.createAppForm.get('headline');
      }
    
    //replace
      getBodyTextFormData() {
        return <FormArray>this.createAppForm.get('bodyText');
      }
    //by
      get bodyTextFormData() {
        return <FormArray>this.createAppForm.get('bodyText');
      }
    
    //replace
      getLocationFormData() {
        return <FormArray>this.createAppForm.get('location');
      }
    //by
      get locationFormData() {
        return <FormArray>this.createAppForm.get('location');
      }
    
    //and replace
      getopeningHoursFormData() {
        return <FormArray>this.createAppForm.get('location')?.get('openingHours');
      }
    //by
      get openingHoursFormData() {
        return <FormArray>this.createAppForm.get('location')?.get('openingHours');
      }
    

    You can use this.headlineFormData instead this.getHeadlineFormData() -see that using a getter you don't write parentesis. and in html headlineFormData.controls instead getHeadlineFormData().controls -again see that using a "getter" you don't write parenthesis.

    Your forked stackbliz