Angular losing binding after array update - arrays

I have a situation where i need to have a dynamic array, think like a booking form, where people are adding flights to the list but it could be 1 flight or 10 flights. i did up a dummy example where i have been able to replicate the issue, i'm hoping it's me and not an issue with the Angluar. anyway, here it is.
I start with an empty array with a button to add items, those items are bound with a *ngFor (code below) each of the fields below are in that array, the "Name" values i have populated 1-5 by just typing
I then Decide to delete number 3 which is successful
I then decide to add a new one, here is where everything goes wrong. as you can see below, it successfully adds the 5th item again, but the one that should have #5 in it, is now blank.
I then press "Create Array" which just dumps the array to console, and i see the below, the values are still in there, but not bound to the Input for that 1 item.
Ok, Now for the code:
This is my HTML Template file:
<form name="form" #f="ngForm">
Name: <input class="input" type="text" name="Name" [(ngModel)]="model.Name"
#Name="ngModel" />
Description: <input class="input" type="text" name="Description"
[(ngModel)]="model.Description" #Description="ngModel" />
<br>
<button (click)="addThought()">New Thought</button>
<div class="Thought" *ngFor="let Thought of myObject.Thoughts;let i=index">
Thought Name:<input name="Name-{{i}}" [(ngModel)]=Thought.Name
#Thought.Name="ngModel" type="Text" /><br>
Thought Description:<input name="Description-{{i}}"
[(ngModel)]=Thought.Description #Thought.Description="ngModel" type="Text"
/>
<br>
<br>
<button (click)="removeThought(Thought)">Remove Thought</button>
</div>
<button (click)="CreateThought()">Create Arrays</button>
</form>
and this is my component TS file:
export class CreateThoughtComponent implements OnInit {
model: any = {};
myObject: customObject = new customObject;
constructor(private guid: Guid, private staticData: StaticDataService) {
}
ngOnInit() {
}
CreateThought() {
console.log(this.myObject);
}
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
}
removeThought(t: Thought) {
this.myObject.Thoughts = this.myObject.Thoughts.filter(item => item !==
t);
}
}
And here is the declaration of the array within an object
export class customObject {
Name: string;
Description: string;
Thoughts: Thought[];
constructor() {
this.Thoughts = new Array<Thought>();
}
}
export class Thought {
Name: string;
Description: string;
}
Any help or suggestions would be greatly appreciated.

This is a tricky thing about Angular's change detection mechanism. You can solve your problem easily by creating a clone of your object. e.g.
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
// clone the object in order to force Angular to apply changes
this.myObject = JSON.parse(JSON.stringify(this.myObject));
}

I solved it by removing the name="Name-{{i}}" from the input's, and adding [ngModelOptions]="{standalone: true}" instead. at it seemed to be an issue with the dynamic way i was assigning the Name to the input using the "index"
I was able to solve it by randomly generating a guid for each name but that created a mire of other issues as well.
That being said, i have also tested DiabolicWord's solution above and it works, since it's so simple going to mark his as the answer.

Related

Complex Tables in Angular (How to create dynamic)

In my current project, we have to show complex tables like you see here: https://medium.muz.li/complex-tables-356826d11861 (or see image).
The cells are very dynamic, and all information coming from API. We have about 25 different tables, with the same features and behavior. The solution right now is a component for each table. I found this to be no got approach, as this is not very dynamic and chances are, the codebase of the component could differentiate and so we will have difficulties adding new features, changes, and solving bugs overall tables.
As I understand to solve this in terms of UI I was thinking about the best approach, how the basic building should look like. Like we could create an Array for each table which holds the row, which holds the cells as objects and these objects hold all information about each cell (editable, style, content type, tooltip...).
Advantages: Dynamic, one codebase for all (upcoming) tables, faster implementation of features and changes, ease of manipulating the table (filter, search, sorting, remove/add rows).
What would you suggest?
Your approach is totally fine and surely the way to go.
If you want to go a bit further, you can create components for each type of cells :
TextCell, TextfieldCell, SelectCell, etc ...
and even further have your components loaded dynamically
have a look at ComponentFactoryResolver
then in your data cell Object, add a field with the name of the component.
It's not the easiest to do though but the benefits will be here in term of maintainability
Expanding on Jeremy's answer, you could implement
import { Component } from "#angular/core";
export class Table {
constructor(
public title: string,
public columns: string[],
public rows: Row[]
) {}
}
export class Row {
constructor(public cells: Cell[]) {}
}
export type CellValue = string | number | boolean;
export class Cell {
constructor(public value: CellValue) {}
isText(): boolean {
return typeof this.value === "string";
}
isNumber(): boolean {
return typeof this.value === "number";
}
isBoolean(): boolean {
return typeof this.value === "boolean";
}
}
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
title = "CodeSandbox";
table: Table;
constructor() {
this.table = new Table(
"Table name",
["Country", "State", "Population", "Northern"],
[
new Row([new Cell("AR"), new Cell("BSAS"), new Cell(44490000), new Cell(false)]),
new Row([new Cell("US"), new Cell("CA"), new Cell(328000000), new Cell(true)])
]
);
}
}
and have the template be something like the following:
<table>
<tr>
<th *ngFor="let col of table.columns">{{col}}</th>
</tr>
<tr *ngFor="let row of table.rows">
<td *ngFor="let cell of row.cells">
<input type="text" *ngIf="cell.isText()" [(ngModel)]="cell.value" />
<input type="number" *ngIf="cell.isNumber()" [(ngModel)]="cell.value" />
<input
type="checkbox"
*ngIf="cell.isBoolean()"
[(ngModel)]="cell.value"
/>
</td>
</tr>
</table>
These values will be automatically filled as you write the input because of the [(ngModel)]. Hope it helps you. I created a sample here in case you want this code.

Angular 5 build form array from JSON result

I am having the following issue with Angular form arrays, I was wondering if someone could help me out as I am quite new with Angular?
Apologies I cannot provide a plunker due to the complexity of the project (lots of dependencies and complex code), but I will do my best to provide as much detail as I can!
I have a JSON response from a service call that contains a group of fields (called "myFields") such as:
0:
name: "field1"
1:
name: "field2"
I am getting this response from a call to an API, and I need to build a form using the fields from the reponse. I am currently looping through this response and attempting to build a form array as follows:
constructor(private formBuilder: FormBuilder){
this.myFormGroup = this.formBuilder.group({
aliases: this.formBuilder.array([
])
});
}
get aliases() {
return this.myFormGroup.get('aliases') as FormArray;
}
getServiceFields(){
*call to get fields and store in "myFields"*
for (let item of myFields) {
this.aliases.push(this.createGroup(item));
}
}
createGroup(item): FormGroup {
return this.formBuilder.group({
name: new FormControl(item.name)
});
}
And in my view I have:
<div [formGroup]="myFormGroup" class="example-form">
<div formArrayName="aliases" >
<div *ngFor="let field of myFormGroup.controls.aliases.controls;
let i=index">
<mat-form-field>
<input matInput placeholder="{{field.value.name}}"
formControlName="{{field.value.name"}}>
</mat-form-field>
The issue I am having is that nothing shows on the page and this is the error I see in the console window:
Error: Cannot find control with path: 'aliases -> name'
I will also attach a screenshot showing the structure of my FormGroup in the console window:
FormGroup structure
Hopefully this is enough information, if additional details are required I can provide them. Anyone have an idea where I am going wrong? Thanks!
Edit: I cannot hard code the formControlName (e.g formControlName="name") as I am looping through the list of controls in "aliases", this is why I am trying to use {{field.value.name}}
<div *ngFor="let field of myFormGroup.controls.aliases.controls;
let i=index">
<div [formGroup]="field">
<mat-form-field>
<input matInput placeholder="{{field.value.name}}"
formControlName="name">
</mat-form-field>
</div>
replace above code in your html.
Problem is you are not binding formgroup before formcontrolname. formcontrolname should work under formgroup.
Please let me know if you have any question.

Why does my <a> taghelper route go dead after its route is used by a form post?

This is a ASP.Net Core MVC project.
In my layout file I have the following Menu link:
<li><a asp-controller="Employees" asp-action="Index">Employees</a></li>
The MVC controller it routes to looks like this:
public IActionResult Index()
{
return View();
}
When I click the link the Action is hit and the view is rendered with my Employee List View.
The Employee List View is bound to an Angular Controller which calls a corresponding Employees Web API GET and the view shows my employee list unfiltered.
Great.
Now we need an Employee quick search from a quick search panel.
So I modify my MVC Employees controller like this:
public IActionResult Index(EmployeeListPageViewModel empListPageVM)
{
return View(empListPageVM);
}
It takes in this Model:
public class EmployeeListPageViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
My Quick Search Form looks like this back in the layout file:
<form asp-controller="Employees" asp-action="Index">
<th>Employee Search:</th>
<td><input id="firstName" name="firstName" type="text" placeholder="FirstName" /></td>
<td><input id="lastName" name="lastName" type="text" placeholder="LastName" /></td>
<td>
<button class="btn btn-xs btn-primary glyphicon glyphicon-search">
</button>
</td>
</form>
Now the model is built from the form and sent to my MVC Employees Index action.
And of course I roll all the needed changes through.
Make my Employees Web API controller take in optional params.
FirstName = null, LastName = null.
The employee list view takes in the ViewModel:
#model EmployeeListPageViewModel
Binds to the Angular Controller:
ng-controller="employeesController
Calls getEmployees:
ng-init="getEmployees('#Model.FirstName', '#Model.LastName')
The Angular controller works out whether everything is null or filtering is needed:
/***** List Employees *****/
$scope.getEmployees = function (pfirstName, pLastName) {
var config = {
params: {
firstName: pfirstName,
lastName: pLastName
}
}
$http.get(employeeUrl, config)
.then(function (response) {
// Test front end exception message;
// throw "test exception";
$scope.data.employees = response.data;
})
.catch(function (error) {
$scope.data.employeeListError = error;
});
}
Hope all of this makes sense. Just laying the foundation here.
Now my problem:
Everything seems to work individually.
But, when I go in fresh and click the Employees Menu Link I get my full list.
And when I fill in FirstName And/or LastName in the quick search it works.
But now the Employees menu link is dead. It doesn't fire. It doesn't hit the Employees Index Controller action.
What is it about the form that is killing the Menu Link?
Update 1: After thinking about this I believe the anchor tag helper is looking at the controller and index and saying, "I am already there." So it is not going to the controller action. How do I force it to go even if it is already there?
Update 2: I tried changing the link to this:
<li>Employees</li>
The link works but it is still killed after the form post.
Apparently, no matter how you direct to the link, taghelper, ng-href, straight link, whatever, if you are already there the link will not go.
I had to replace the anchor link in the menu with this:
<li>
<form asp-controller="Employees" asp-action="Index">
<button type="submit" class="navbarLinks">
Employees
</button>
</form>

How to call a function on uncheck and on check with Aurelia

I have a list of items coming in from an API and they won't always be the same, so the number of items in the array is always changing. I'm creating a checkbox for each item.
The user has the ability to check/uncheck each item. Here's what I want to do:
When an item is checked, it will push the ID of that item into an array
When an item is unchecked, it will remove the ID of that item from the array
I just need to know how I call something based on whether it was checked or unchecked. I've tried a "checked.delegate" and a "checked.trigger" and I can't seem to get that to work.
Just a regular click.delegate won't work because I can't keep state on whether it's true or false and I can't set variables for all of them because I don't always know which items are going to be coming in from the API. Any suggestions?
Try change.delegate or change.trigger like this:
VM method:
logchange(value) {
console.log(value);
}
View:
<input type="checkbox" change.delegate="logchange($event.target.checked)" />
There is (since when?) official documentation of how to solve exactly this specific problem cleanly: https://aurelia.io/docs/binding/checkboxes#array-of-numbers
No need to handle events!
Aurelia can bind directly to your array and handle everything for you - all you need to do is tell Aurelia what property of the elements you are repeating over to store in the array (the id).
The gist of it:
app.js
export class App {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProductIds = [];
}
app.html
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="checkbox" model.bind="product.id" checked.bind="selectedProductIds">
${product.id} - ${product.name}
</label>
<br>
Selected product IDs: ${selectedProductIds}
</form>
</template>
No other code is needed.
One way you can do this is with the help of a setter. Let's say you have a checkbox like this:
<input type="checkbox">
Create a private field in your View Model and then wrap it with a getter and a setter:
get isChecked(){
return this._isChecked;
}
set isChecked(value){
this._isChecked = value;
//enter your extra logic here, no need for an event handler
}
private _isChecked: boolean;
Then bind isChecked to the view:
<input type="checkbox" checked.bind="isChecked">
Every time the checkbox is checked or unchecked the setter will be called and you can call any method you want from within the setter.
Another more unconventional way to achieve this is by using the #bindable decorator like this:
#bindable isChecked: boolean;
It's unconventional because you probably don't want isChecked to be bindable but the decorator gives you access to the isCheckedChanged method:
isCheckedChanged(newValue, oldValue){
//Logic here
}
And of course there is the change event which you can catch with change.trigger and change.delegate but that has already been mentioned in another answer

Post full form data to a service in Angular

I have a form that contains a lot of fields and I want to post all the form fields to a service using a post method. But I would like to send the whole form object and not to write one property by one. If I try to post the object that contains all my fields $scope.formData it also contains all the angular stuff inside like errors. What I need is a collection of field names and values. How can I achieve this with minimum coding?
Edit:
I ended up writing my own function:
function getAngularFormFields(form) {
var dictionary = { form: {} };
for (var key in form) {
if (form.hasOwnProperty(key) && !key.indexOf('$') == 0) {
dictionary.form[key] = form[key].$modelValue;
}
}
return dictionary;
}
Normally if you need to post a form you could just use the default method provided by your browser. This will send the form data, via POST, to your URL.
<form action="yourUrlHere" method="POST">
First name: <input type="text" name="fname">
Last name: <input type="text" name="lname">
<input type="submit" value="Submit">
</form>

Resources