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
);
}
}
}
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!