2

What I would like to achieve: To dynamically create a new formGroup for each recipe I receive from the backend (stored in the this.selectedRecipe.ingredients) and then patch the value of each formControl from the newly created formGroup with the data I received.

What I've tried: Until this point, please see below what I have tried so far and at the moment it works partially, but the problem is that it's being created only one instance of formGroup and only patches the value of the last recipe from the this.selectedRecipe.ingredients array and not for each recipe from the array. Finally, what I would like to achieve is to have an instance of formGroup for each recipe in the this.selectedRecipe.ingredients array, and to patch the value of each formControl from formGroup with information from my array.

If you guys have any idea how I can approach this further or what I'm doing wrong I would really appreciate it. Thanks a lot!

HTML

<form [formGroup]="editRecipeF" (ngSubmit)="onSubmitEditRecipeForm()">
    <div formGroupName="recipeDetails">
      <div class="form-group">
        <label for="title">Title</label>
        <input
          type="text"
          id="title"
          formControlName="title"
          class="form-control"
        />
      </div>
      <div class="form-group">
        <label for="imageUrl">Recipe Image</label>
        <input
          type="text"
          id="imageUrl"
          formControlName="imageUrl"
          class="form-control"
        />
      </div>
    </div>

    <div formArrayName="ingredients">
      <div class="ingredients-div mt-4">
        <h2>Ingredients</h2>
        <button
          class="float-left btn"
          mat-icon-button
          color="primary"
          aria-label="Add"
          (click)="onAddIngredients()"
          matTooltip="Add"
        >
          <p>+</p>
        </button>
      </div>
      <div
        class="row"
        *ngFor="let ingredient of getControls(); let i = index"
        [formGroupName]="i"
      >
        <div class="form-group">
          <label for="name">Ingredient Name</label>
          <input
            class="form-control"
            type="text"
            name="name"
            id="name"
            formControlName="name"
          />
        </div>
        <div class="form-group">
          <label for="qty">Ingredient Qty</label>
          <input
            class="form-control"
            type="text"
            name="qty"
            id="qty"
            formControlName="qty"
          />
        </div>
      </div>
    </div>
    <button class="btn btn-primary mt-4" type="submit">Submit</button>
  </form>

TypeScript

@Component({
  selector: "app-edit-recipe",
  templateUrl: "./edit-recipe.component.html",
  styleUrls: ["./edit-recipe.component.css"],
})
export class EditRecipeComponent implements OnInit {
  editRecipeF!: FormGroup;
  recipeId: any;
  selectedRecipe!: Recipe;

  constructor(
    private formBuilder: FormBuilder,
    private recipesService: RecipesService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.recipeId = this.route.snapshot.params["id"];

    this.editRecipeF = this.formBuilder.group({
      recipeDetails: this.formBuilder.group({
        title: ["", Validators.required],
        imageUrl: ["", Validators.required],
        duration: ["", Validators.required],
        calories: ["", Validators.required],
      }),
      ingredients: this.formBuilder.array([this.createIngFormGroup()]),
    });

    this.recipesService.fetchRecipeDetails(this.recipeId).subscribe(
      (selectedRecipeDetails) => {
        this.selectedRecipe = selectedRecipeDetails;

        this.editRecipeF.patchValue({
          recipeDetails: {
            title: this.selectedRecipe.title,
            imageUrl: this.selectedRecipe.imageUrl,
            duration: this.selectedRecipe.duration,
            calories: this.selectedRecipe.calories,
          },
        });

        const ing = this.editRecipeF.get("ingredients") as FormArray;
        for (let ingredient of this.selectedRecipe.ingredients) {
          ing.patchValue([
            {
              name: ingredient.ingName,
              qty: ingredient.ingQty,
              qtyUnit: ingredient.ingQtyUnit,
              imageUrl: ingredient.ingImageUrl,
            },
          ]);
        }
      },
      (error) => {
        console.log(error);
      }
    );
  }

  private createIngFormGroup() {
    return new FormGroup({
      name: new FormControl("", Validators.required),
      qty: new FormControl("", Validators.required),
      qtyUnit: new FormControl("", Validators.required),
      imageUrl: new FormControl("", Validators.required),
    });
  }

  public getControls() {
    return (<FormArray>this.editRecipeF.get("ingredients")).controls;
  }

  public onAddIngredients() {
    const ingredients = this.editRecipeF.get("ingredients") as FormArray;
    ingredients.push(this.createIngFormGroup());
  }

  public onSubmitEditRecipeForm() {
    if (this.editRecipeF.valid) {
      console.log(this.editRecipeF.value);
      this.recipesService
        .editRecipe(this.editRecipeF.value, this.recipeId)
        .subscribe(
          (success) => {},
          (error) => {
            console.log(error);
          }
        );
    }
  }
}
0

1 Answer 1

1

you need path value to each element of the FormArray

    const ing = this.editRecipeF.get("ingredients") as FormArray;
    ing.clear(); //<--remove all elements of the formArray;
    for (let ingredient of this.selectedRecipe.ingredients) {
      let i=ing.length;
      ing.insert(this.createIngFormGroup()) //<--add an empty formGroup
      ing.at(i).patchValue([  //<--see the at(i)
        {
          name: ingredient.ingName,
          qty: ingredient.ingQty,
          qtyUnit: ingredient.ingQtyUnit,
          imageUrl: ingredient.ingImageUrl,
        },
      ]);
    }

You create a FormArray with so many elements as ingredients

    const ing = this.editRecipeF.get("ingredients") as FormArray;
    ing.clear(); //<--remove all elements of the formArray;

    //create so many elements as increadients
    for (let ingredient of this.selectedRecipe.ingredients) {
      let i=ing.length;
      ing.insert(this.createIngFormGroup()) //<--add an empty formGroup

    //make the pathValue
    ing.at(i).patchValue(  //<--see the at(i) and remove the "["
        {
          name: ingredient.ingName,
          qty: ingredient.ingQty,
          qtyUnit: ingredient.ingQtyUnit,
          imageUrl: ingredient.ingImageUrl,
        },
      );
    }

But we can even make the things better. PathValue makes that our FormGroup has the values we indicate. So we can make perfectly make one unique pathValue. Some like

this.editRecipeF.patchValue({
    recipeDetails: {
            title: ..,
            imageUrl: ...,
            ...
            },
    ingredients:[
            {
              name:...
              qty: ...
              ...
            },    
            {
              name:...
              qty: ...
              ...
            },    
            {
              name:...
              qty: ...
              ...
            },    
    ]
  })

It's true that, when we receive the data we can check how many elements has our FormArray, add/remove the neccesary and make the pathValue

But the problem when we has a FormArray is that it's necesary our formArray has so many elements than the "ingredients" we has (if our array has less, path value don't add elements)

I suggest another aproach. Imagine you change a bit your function createIngFormGroup() to allow received an object to create the formGroup with the data. A classic is

private createIngFormGroup(data:any=null) {
    //if not pass data as argument create a data "on fly"
    //with values by defect
    data=data || {ingName:null,ingQty:0,ingQtyUnit:null,ingImageUrl:null}

    return new FormGroup({
      name: new FormControl(data.ingName, Validators.required),
      qty: new FormControl(data.ingQty, Validators.required),
      qtyUnit: new FormControl(data.ingQtyUnit, Validators.required),
      imageUrl: new FormControl(data.ingImageUrl, Validators.required),
    });
  }

So

  this.createIngFormGroup() //you get a FormGroup with empty values

  this.createIngFormGroup({ //you get a FormGroup with the values indicated
     ingName:"sugar",
     ingQty:20,
     ingQtyUnit:'spoon',
     ingImageUrl:'/images/sugar.jpg'

  })

Futhermore, we can create a function that return a formGroup type "editRecipeF" with the same idea that use our function. I use againg FromGroup, not FormBuilder to give "coherence" to our code. If we are using FormBuilder, we should using formBuilder in all the functions of the component, else in no where

private createForm(data:any=null){
  data=data || {recipeDetails: {title:null,imageUrl:null,duration:0,calories:0},
                ingredients:[null]}

  return new FormGroup({
     recipeDetails:new FormGroup({
       title:new FormControl(data.recipeDetails.title,Validators.required),
       imageUrl:new FormControl(data.recipeDetails.imageUrl,Validators.required),
       duration:new FormControl(data.recipeDetails.duration,Validators.required),
       calories:new FormControl(data.recipeDetails.calories,Validators.required)
     }),
     ingredients:new FormArray(data.ingredients.map(x=>this.createIngFormGroup())
  })
}

see two important things.

First the value of defect of our "ingredients", we are choose that will be an array of one element with null value

The way to create the FormArray. Our formArray is a FormArray of FormGroup, so we transform the array data.ingredients in an array of formGroup. This is the "map" make:

data.map(x=>this.createIngFormGroup() //--is an array of FormGroup

and we enclosed in new FormArray(..our array of FormGroups..)

Well, it's all ready for us. We can make in ngOnInit

ngOnInit()
{
    this.editRecipeF=this.createForm()
}

Or we can, in subscribe make

 this.recipesService.fetchRecipeDetails(this.recipeId).subscribe(
      (res:any) => {
        this.editRecipeF=this.createForm(res)

 })

It's all.

Important note. when you recieve the data of ingredients the variables are ìngName, ingQty, ingQtyUnit and ingImageUrl, but the elements of your formArray are name,qty,qtyUnit and imageUrl (without the "ing"). I feel it's better that the variables of the elements of your form array has the prefix "ing" (so more like was to the data of your dbs it's better, else you need transform with/without prefix to makes the call to your API)

Sign up to request clarification or add additional context in comments.

5 Comments

Hi @Eliseo! I'm really sorry for my late reply. Thanks a lot for your detailed answer, it means a lot for a person learning to code. The very first option should work just with that? I tried the very first approach. I had 1 issue with the insert. I had to add the index as well like: ing.insert(0, this.createIngFormGroup());. However, it creates in HTML new form controls as expected, but everything is empty. No patched value. Do you have any idea why this might happen?
sorry, in the code it's wrong, is push(..) not ìnsert() -as you say, insert need the "index where you insert the new FormGroup-, you should use the index "i" to insertted:ing.insert(i,this.createIngFormGroup())
not sure why it's not working with the first approach.. it creates form controls (empty) correctly for the number of ingredients in HTML but no patchValue, but if I console.log(ingredient.ingName) I see in the console the right information. This using just the first approach
@alex, sorry again, You need remove the "[" "]". You path value one value each time. I corrected in the answer and make a simple stackblitz with your code modified
thank you @Eliseo! it works now and it means a lot for a person who wants to learn to code that you got the time to explain and also how to improve the code with the second version. Thanks a lot!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.