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
.
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.