How should I set the value for my model using a select control when the bound model is a different type from my select option?

angular angular-material asp.net-core entity-framework-core typescript

Question

I've been using AngularJS for a long time and have recently been diving into the new Angular and .NET Core. With my new endeavor, I began learning the vast changes in Entity Framework Core. One of the most impactful changes relates to many-to-many relationships. Previously, you had the ability to hide the join table which aligned nicely with AngularJS, because the navigation property on the ngModel could be the same type as the option value. Now that this is no longer a thing, we must find a means of converting from one entity to another. Here's my models:

Transaction.cs

public class Transaction
{
    public int Id { get; set; }

    public DateTime Date { get; set; }

    public string AccountNumber { get; set; }

    public string Key { get; set; }

    public decimal Amount { get; set; }

    public string Description { get; set; }


    public virtual IList<CategoryTransaction> CategoryTransactions { get; set; }
}

Category.cs

public class Category
{
    public int Id { get; set; }

    public string Name { get; set; }


    public virtual IList<CategoryTransaction> CategoryTransactions { get; set; }
}

CategoryTransaction.cs

public class CategoryTransaction
{
    public int CategoryId { get; set; }

    public int TransactionId { get; set; }


    public virtual Category Category { get; set; }

    public virtual Transaction Transaction { get; set; }
}

I'm using TypeScript and have defined my models and my markup form looks like this:

<form>
  <mat-form-field>
    <mat-select [compareWith]="compareFn" placeholder="Categories" [(ngModel)]="testTransaction.categoryTransactions" multiple name="foo">
      <mat-option *ngFor="let category of categoryList" [value]="category">{{ category.name }}</mat-option>
    </mat-select>
  </mat-form-field>
</form>

and here is my compareFn:

compareFn(category: Category, categoryTransaction: CategoryTransaction) {
    return categoryTransaction && category ? categoryTransaction.categoryId === category.id : false;
}

I got here and started thinking: "How am I going to convert from Category to CategoryTransaction? I was looking around for ways to sorta "hook into" the model setter event and create a CategoryTransaction object using the selected Category, but I haven't been able to find a means of hooking in. Then I saw in a post that you could use JSON in the value attribute. So I altered my markup and compare function to this:

<form>
  <mat-form-field>
    <mat-select [compareWith]="compareFn2" placeholder="Categories" [(ngModel)]="testTransaction.categoryTransactions" multiple name="foo">
      <mat-option *ngFor="let category of categoryList" [value]="{ categoryId: category.id, transactionId: 1 }">{{ category.name }}</mat-option>
    </mat-select>
  </mat-form-field>
</form>

and

compareFn2(c1: CategoryTransaction, c2: CategoryTransaction) {
    return c1 && c2 ? c1.categoryId === c2.categoryId : false;
}

It works, but I don't like it. It feels messy/hackish and doesn't seem to fit into the "Angular Way" where we're working with objects. I don't think the view should be where we convert values. What I imagined I would be doing would be "new-ing up" a CategoryTransaction and setting the value or implementing some value converter. Since I would be converting/setting the value in TypeScript, if my model changes, the TypeScript compiler would hopefully complain if I missed it.

So this seems like a common scenario (many-to-many) that people must've encountered with EF Core + Angular, but I'm having issues finding a good example. Has anyone found a good way to handle this scenario or perhaps has a good example for overriding the ngModel value using a select control?

I'm using .NET Core 2.1, Angular 7.0.4, Material 7.0.4, and EF Core Sql Server 2.1.4.

1
1
1/7/2019 6:44:11 PM

Accepted Answer

Using the <mat-select> I decided to subscribe to the selectionChange event and see what it would provide me.

<form>
  <mat-form-field>
    <mat-select [compareWith]="compareFn" placeholder="Categories" [(ngModel)]="testTransaction.categoryTransactions" multiple name="foo" (selectionChange)="categoryChange($event, testTransaction)">
      <mat-option *ngFor="let category of categoryList" [value]="category">{{ category.name }}</mat-option>
    </mat-select>
  </mat-form-field>
</form>

I passed it the $event argument and the Transaction object.

// Respond to when the selection changes
categoryChange(event: MatSelectChange, transaction: Transaction) {
    // Rather than trying to manually detect what changed, just clear out all the options
    transaction.categoryTransactions = [];
    // Iterate through all the selected options
    for (var i: number = 0; i < event.value.length; i++) {
      // Add a new CategoryTransaction for each selected Category
      transaction.categoryTransactions.push({
        transactionId: transaction.id,
        categoryId: event.value[i].id
      });
    }
}

The $event object parameter passes all the selected Category objects and the Transaction parameter is the object that holds all the join table entries. I was afraid that by changing the transaction.categoryTransactions which is bound to the ngModel I might be stuck in an infinite loop of the selectionChange event firing, but to my relief, that didn't turn out to be the case.

0
1/8/2019 1:58:36 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow