Angular 6 Reactive Forms - Set the selected <option> of a <select> FormControl dynamically by condition

Viewed 42387

I need to know how I set the selected <option> of a <select> FormControl dynamically by condition.

Let’s say we have customers with orders. There is a dropdown menu where we select a customer and depending on that customer the options for the orders dropdown menu should be set dynamically and a specific order should be selected.

Also both dropdown menus have an <option> “(new customer/order)” in case there is not yet a customer or an order, so if we route to that form without having a customer, this “new” option should be selected in both drop down menus. If we have a customer without orders, the “new” option should be selected in the orders dropdown menu.

This is the form.component.html:

<form [formGroup]="customerForm">
    <select formControlName="customers">
        <option>(new customer)</option>
        <option [ngValue]="customer" *ngFor="let customer of customers">
            {{customer.name}}
        </option>
    </select>
    <select formControlName="orders">
        <option>(new order)</option>
        <option [ngValue]="order" *ngFor="let order of filteredOrders">
            {{order.id}}
        </option>
    </select>
</form>

This is the form.component.ts:

customerForm: FormGroup;

customers: Customer[] = [
{   "id": 1,
    "name": "Meghan Castellano",
    "orderIds": [1,2] },
{   "id": 2,
    "name": "Monika Weiss",
    "orderIds": [3,4] }];

orders: Order[] = [{"id": 1},{"id": 2},{"id": 3},{"id": 4}];

filteredOrders: Order[];

constructor( private route: ActivatedRoute,
            private fb: FormBuilder) {}

ngOnInit() {
    this.customerForm = this.fb.group({
        customers: '',
        orders: '' });

    let customerId = this.route.snapshot.getParam('id');

    if (customerId == 0) {
        this.customerForm.patchValue({
            customers: "(new customer)",
            orders: "(new order)" });
    }
    else
    {
        let customer = this.customers[customerId];

        this.customerForm.patchValue({ customers: customer.name });

        if (customer.orderIds.length != 0) {
            this.filteredOrders = getCustomersOrders(customer);

        this.customerForm.patchValue({
            orders: this.filteredOrders[0].id }
        }
    }
}

getCustomersOrders(customer: Customer): Order[] {
    let orders: Order[];

    for (var id = 0; id < customer.orderIds.length; id++) {
        let orderId = customer.orderIds[id];
        let order = this.orders.find(i => i.id == orderId);
        orders.push(order); }

    return orders;
}

Currently I'm routing to the form and provide an id with the url. The customer dropdown menu selects the correct customer depending on the id in the url.

However, the order dropdown menu is only populated correctly, but the selection does not work. No option at all is selected.

If I replace the value in this statement:

this.customerForm.patchValue({ orders: this.filteredOrders[0].id }

with the "(new order)" string already specified in the html:

this.customerForm.patchValue({ order: "(new order)" })

it works. The string "(new order)" is selected in the orders dropdown menu. It just don't work for the filteredOrders.

What am I doing wrong here? Do I maybe need a whole different approach here?

3 Answers

Okay, after a couple of hours I got it. Here are my updated .html and .ts files for your interest:

HTML:

<form class="form-container" [formGroup]="customerForm">
    <select formControlName="customer" (change)="onCustomerChanged($event)">
        <option>(new customer)</option>
        <option *ngFor="let customer of customers" [value]="customer.name">
            {{customer.name}}
        </option>
    </select>
    <select formControlName="order">
        <option>(new order)</option>
        <option *ngFor="let order of filteredOrders" [value]="order.id">
            {{order.id}}</option>
    </select>
</form>

.ts-file

export class CustomerFormComponent implements OnInit {

  customerForm: FormGroup;
  selectedCustomer: Customer;
  filteredOrders: Order[];

  customers: Customer[] = [...];
  orders: Order[] = [...];  

  constructor(private route: ActivatedRoute,
              private fb: FormBuilder) { }

  ngOnInit() {
    this.customerForm = this.fb.group({
      customer: "",
      order: ""
    });

    let customerId = Number(this.route.snapshot.paramMap.get('customerId'));
    this.setFormControlValues(customerId);
  }

  setFormControlValues(customerId: number) {
    if (customerId == 0) {
      this.customerForm.get('customer').setValue("(new customer)");
      this.customerForm.get('order').setValue("(new order)");
    }
    else  {
      this.selectedCustomer = this.customers.find(i => i.id == customerId);
      this.filteredOrders = this.getCustomerOrders(this.selectedCustomer);

      this.customerForm.get('customer').setValue(this.selectedCustomer.name);
      this.customerForm.get('order').setValue(this.filteredOrders[0].orderNumber);
    }
  }

  getCustomerOrders(customer: Customer) : Order[] {
    let orders: Order[] = [];

    for (var id = 0; id < customer.orderIds.length; id++)  {
      let orderId = customer.orderIds[id];
      orders.push(this.orders.find(i => i.id == orderId));
    }

    return orders;
  }

  onCustomerChanged(event: any) {
    this.selectedCustomer = this.customers.find(n => n.name == event.target.value);

    this.setFormControlValues(this.selectedCustomer.id);
  }
}

As you can see, in the HTML i now use "[value]" instead of "[ngValue]" and "customer.name" / "order.id" instead of only "customer" / "order".

In the .ts-file I got rid of the "patchValue()" method and brought in the "setValue" method.

Here is the working stackblitz-example:

https://stackblitz.com/edit/angular-conditionaldropdown-gnajge

I think that the problem comes from the object that is selected in the order selector (also in the customers one, in fact).

<option [ngValue]="order" *ngFor="let order of filteredOrders">
    {{order.id}}
</option>

This means that the whole order object will be selected and not only the order id. In this case, if you try to patch the value only with the order.id, then it won't work : the selector wait for an Order object, not the order.id.

I put together a quick stackblitz with a working version of the code you put in the question: https://stackblitz.com/edit/angular-drop-dowb. The only real difference is that

this.customerForm.patchValue({
    orders: this.filteredOrders[0]
});

is used instead of

this.customerForm.patchValue({
    orders: this.filteredOrders[0].id }
});

Update

So, i've updated the stackblitz.

To keep the orders field with the correct values along the customer, you'll need to add a (change) in the select field.

<select formControlName="customers" (change)="updateOrders($event)">
    <option [ngValue]="null">(new customer)</option>
    <option [value]="customer.id" *ngFor="let customer of customers" >
        {{customer.name}}
    </option>
</select>

Don't forget to pass the customer id field as the value instead of the customer - Angular does not seem to like it. With the id value then, you'll need to add a updateOrders function in the component.ts.

  updateOrders(event) {
    const index = this.customers.findIndex(item => item.id == event.target.value);
    if (index === -1) {
      this.customerForm.controls.orders.setValue(null);
      return;
    }
    this.filteredOrders = this.getCustomersOrders(this.customers[index]);
    this.customerForm.controls.orders.setValue(this.filteredOrders[0]);
  }

In the case the id is not in the customers list, then you'll only update the orders with a null value, corresponding to tyour (new order). And if the id is in the customers list, then you'll update the filteredOrders.

If you're using [ngValue] you can use [compareWith] on your select element passing in a comparison function to determine if the value is selected. Using [ngValue] allows for complex objects over [value] which is a string.

<form [formGroup]="customerForm">
    <select [compareWith]="compareCustomers" formControlName="customers">
        <option>(new customer)</option>
        <option [ngValue]="customer" *ngFor="let customer of customers">
            {{customer.name}}
        </option>
    </select>
    <select [compareWith]="compareOrders" formControlName="orders">
        <option>(new order)</option>
        <option [ngValue]="order" *ngFor="let order of filteredOrders">
            {{order.id}}
        </option>
    </select>
</form>
export class SomeComponent {
  public compareCustomer(a: Customer, b: Customer): boolean {
    return a.id === b.id;
  }

  public compareOrders(a: Order, b: Order): boolean {
    return a.id === b.id;
  }
}
Related