jsonangularformsangular-reactive-formsdynamic-forms

Angular Dynamic Reactive select option form


I'm building a multi-step form in Angular where users can create a Dungeons & Dragons character. In one of the steps, after the user selects a class (Bard, Cleric), they need to choose one equipment option from a list provided by a dummy data.ts file. Each equipment choice offers either an item or an alternate option.
I need to dynamically populate the equipmentChoice field in the form based on the selected class. Once populated, the user should be able to choose either the item or the alternate for each equipment choice, if item is selected, alternate shoud be disabled.

How can I implement this? Have no idea how to do it. Should i change JSON object, or try something else?

data.ts

export const equipment: any[] = [
  {
    class: "Barbarian",
    equipment_choices: [
      { item: "greataxe", alternate: "any martial melee weapon" },
      { item: "two handaxes", alternate: "any simple weapon" },
    ],
    standard_equipment: ["explorer's pack", "four javelins"],
  },
  {
    class: "Bard",
    equipment_choices: [
      {
        item: "rapier",
        alternate: ["longsword (if proficient)", "any simple weapon"],
      },
      { item: "diplomat's pack", alternate: "entertainer's pack" },
      { item: "lute", alternate: "any other musical instrument" },
    ],
    standard_equipment: ["leather armor", "dagger"],
  },
  {
    class: "Cleric",
    equipment_choices: [
      { item: "mace", alternate: "warhammer (if proficient)" },
      {
        item: "scale mail",
        alternate: ["leather armor", "chain mail (if proficient)"],
      },
      { item: "light crossbow and 20 bolts", alternate: "any simple weapon" },
      { item: "priest's pack", alternate: "explorer's pack" },
    ],
    standard_equipment: ["shield", "holy symbol"],
  },

creator.component.ts

import { Component } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  Validators,
  FormArray,
  FormControl,
} from "@angular/forms";
import {
  dndClasses,
  dndRaces,
  dndBackgrounds,
  dndRacialBonuses,
  classArchetypesWithSpells,
  classSkills,
  fightingStyles,
  equipment,
} from "./data";

@Component({
  selector: "app-creator",
  templateUrl: "./creator.component.html",
  styleUrls: ["./creator.component.scss"],
})
export class CreatorComponent {
  // Step and points management
  currentStep = 1;
  abilityPoints = 27;
  maxAbilityPoints = 27;

  // Data sources
  classes = dndClasses;
  races = dndRaces;
  backgrounds = dndBackgrounds;
  racialBonuses = dndRacialBonuses;
  archetype = classArchetypesWithSpells;
  fightingStyles = fightingStyles;
  skills = classSkills;

  // Form and selected data
  characterCreationForm: FormGroup;
  filteredArchetypes: string[] = [];
  classAndBackgroundSkill: string[] = [];
  selectedSkills: string[] = [];
  selectedEquipment: string[] = [];

  constructor(private fb: FormBuilder) {
    this.characterCreationForm = this.fb.group({
      name: ["", [Validators.required, Validators.minLength(1)]],
      race: ["", Validators.required],
      class: ["", Validators.required],
      background: ["", Validators.required],
      abilityScores: this.fb.group({
        strength: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
        dexterity: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
        constitution: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
        intelligence: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
        wisdom: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
        charisma: [
          8,
          [Validators.required, Validators.min(1), Validators.max(20)],
        ],
      }),
      archeTypes: ["", Validators.required],
      skills: this.fb.array([], Validators.required),
      equipment_choices: this.fb.array([]),
      standart_equipment: this.fb.array([]),
    });
  }

  // Getters
  get skillsFormArray(): FormArray {
    return this.characterCreationForm.get("skills") as FormArray;
  }

  get equipmentFormArray(): FormArray {
    return this.characterCreationForm.get("equipment") as FormArray;
  }

  get equipmentChoicesControls() {
    return (this.characterCreationForm.get("equipment_choices") as FormArray)
      .controls;
  }
   

  selectEquipment() {
    const selectedClass = this.characterCreationForm.get("class")?.value;
    const selectedEquipment = equipment.find(
      (equip) => equip.class === selectedClass
    );

    console.log("Selected equipment:", selectedEquipment);
  }

  onEquipmentChange(event: any, equipment: any) {
    if (event.target.checked) {
      this.selectedEquipment.push(equipment);
      this.equipmentFormArray.push(new FormControl(equipment));
    } else {
      const index = this.selectedEquipment.indexOf(equipment);
      if (index !== -1) {
        this.selectedEquipment.splice(index, 1);
      }
    }
  }

  // Navigation methods
  nextStep() {
    this.selectArchetype();
    this.selectBackground();
    this.getSkills();
    if (this.currentStep < 4) {
      this.currentStep++;
    }
  }

  prevStep() {
    if (this.currentStep > 1) {
      this.currentStep--;
    }
  }

  onSubmit() {
    if (this.characterCreationForm.valid) {
      this.nextStep();
      this.calculateRacialBonus();
      console.log("Form data:", this.characterCreationForm.value);
    } else {
      console.log(
        "Please fill out all required fields.",
        this.characterCreationForm
      );
    }
  }
}

Solution

  • Thanks for reaching out. From what I understood, you're trying to dynamically populate the equipment_choices control based what class the user selects and then allow the user to choose between an item or its alternate. To achieve this, I suggest the following approach:

    Update the selectEquipment method to dynamically populate the equipment_choices control when a class is selected.

    selectEquipment() {
      const selectedClass = this.characterCreationForm.get("class")?.value;
      const selectedEquipment = equipment.find(
        (equip) => equip.class === selectedClass
      );
    
      const equipmentChoicesFormArray = this.characterCreationForm.get('equipment_choices') as FormArray;
      equipmentChoicesFormArray.clear();  // Clear previous selections
    
      if (selectedEquipment) {
        selectedEquipment.equipment_choices.forEach(choice => {
          const choiceGroup = this.fb.group({
            selected: [''],  // Placeholder for the user's choice (either item or alternate)
            itemDisabled: [false],  // Control to disable the item if alternate is selected
            alternateDisabled: [false],  // Control to disable the alternate if item is selected
            item: [choice.item],
            alternate: [choice.alternate]
          });
    
          equipmentChoicesFormArray.push(choiceGroup);
        });
      }
    }
    

    Add a method to handle the disabling of the alternate option when the item is selected and vice versa.

    onSelectionChange(index: number, type: 'item' | 'alternate') {
      const equipmentChoicesFormArray = this.characterCreationForm.get('equipment_choices') as FormArray;
      const choiceGroup = equipmentChoicesFormArray.at(index) as FormGroup;
    
      if (type === 'item') {
        choiceGroup.patchValue({ itemDisabled: false, alternateDisabled: true });
        choiceGroup.get('selected')?.setValue(choiceGroup.get('item')?.value);
      } else {
        choiceGroup.patchValue({ itemDisabled: true, alternateDisabled: false });
        choiceGroup.get('selected')?.setValue(choiceGroup.get('alternate')?.value);
      }
    }
    
    

    Don't forget to bind the form controls in your template. I can't help you much here since I can't reproduce the UI. Here's a simple example with radio buttons.

    <div formArrayName="equipment_choices" *ngFor="let choice of equipmentChoicesControls; let i = index">
      <div [formGroupName]="i">
        <label>
          <input type="radio" formControlName="itemDisabled" [disabled]="choice.get('alternateDisabled')?.value" (change)="onSelectionChange(i, 'item')">
          {{ choice.get('item')?.value }}
        </label>
        <label *ngIf="choice.get('alternate')?.value">
          <input type="radio" formControlName="alternateDisabled" [disabled]="choice.get('itemDisabled')?.value" (change)="onSelectionChange(i, 'alternate')">
          {{ choice.get('alternate')?.value }}
        </label>
      </div>
    </div>
    

    Please let me know if my solution is working for you. Wishing you the best of luck with your project!