Angular input component duplicate ids - javascript

I'm creating a component for a toggle button. The styling is dependant on the input label, so I need to use id and for.
If I have multiple of these components on the page it will clearly mess up... What's the best way to deal with this? Random generate id through a function?
<div class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" id="toggle" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" for="toggle"></label>
</div>

Use index on for:
<div *ngFor="let item of items; let i = index" class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" id="toggle_{{i}}" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" for="toggle_{{i}}"></label>
</div>

This looks like a valid use case for a dynamic ids, as the correct way to associate labels with inputs is through ids.
To do so, add a randNum property to your class that contains a random number created on instantiation. Then, you can use this random number throughout your template to create unique identifiers like so:
Random Number (class property)
randNum = Math.floor(Math.random() * 1000000000);
Template
<div class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" [id]="'toggle' + randNum" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" [for]="'toggle' + randNum"></label>
</div>
Now, if you have this component 10 times on the same page, those 10 instantiations of your component will have unique values of randNum, producing the desired effect.
Edit:
If you'd rather stay away from random numbers, you can ensure uniqueness by defining a static property as the basis for randNum like so:
static num = CustomComponent.num || 0;
randNum = ++CustomComponent.num;
In the code above, each component is guaranteed to have a unique randNum, as it is incremented for every instantiation.

Related

Conditional order in Angular 8

I want to check the condition in Angular.
I want to look at two numbers that would enter Div if these two numbers were equal.
How can I check this condition?
I tried the following method and it was wrong.
<div *ngIf="'{{item1.menuID==item2.menuID}}'">
{{item1.title}}
</div>
The code you have there would evaluate to a string given you're adding single quotes around the condition, also you don't need to interpolate to access an object inside an ngIf.
If you want to evaluate if item1.menuID and item2.menuID are equal you would do
<div *ngIf="item1.menuID == item2.menuID">
{{item1.title}}
</div>
You don't require to use interpolation within [anyAttributeEnclosingSquareBrackets] or any angular attribute like *ngIf, *ngFor or some attributes like formControlName
For example:
<child-component [childAttribute]="parentComponentVariable">
<div *ngFor="let x of arrayVariable">Array Variable is present in component</div>
<input type="text" formControlName="name">
You can directly compare like below code:
<div *ngIf="item1.menuID==item2.menuID">
{{item1.title}}
</div>
Assuming that, item1 and item2 are not private variables of component and are available or initialise expectedly

Angular 6 Nested FormArray

I'm trying to make a form with an array of fields generating by clicking '+' using Angular Reactive Forms. However, when I try to use [formControlName]="index" in child component it doesn't work.
Firstly it was saying that formControlName should have a parent with FormGroup I passed it and added. Now it says Cannot find control name with unspecified name attribute. In the Angular docs it's said that one able to use FormArray with [formControlName] and index since there is no names.
This is what I have
https://stackblitz.com/edit/angular-runxfe
I have fixed your issues. You needed to pass the formGroupName here:
<app-site-form-feature
*ngFor="let feature of features.controls; let last = last; let index = index; let first = first" [formGroupName]="index"
[last]="last" [index]="index" [first]="first"
(addFeature)="addFeature()" (removeFeature)="removeFeature(index, first, last)">
</app-site-form-feature>
HTML of the child component will look like this:
<div class="row no-gutters form-group">
<input type="text" class="form-control px-2 col-10">
<span class="btn btn-outline-danger" (click)="removeFeature.emit(index, first, last)">-</span>
<span class="btn btn-success" *ngIf="last" (click)="addFeature.emit($event,index)">+</span>
</div>
Take a look to this stacblitz https://stackblitz.com/edit/angular-v8zkz2
You can also reference this guide:
https://alligator.io/angular/reactive-forms-formarray-dynamic-fields/

Addition and removal of N-number File Inputs in React

I'm building out something that allows users to upload N-number of data files. In service of this, I've added a button that will create an additional file upload option. This is done with a simple for loop in the render function (there is a selection option that only appears if certain conditions are met, which is the 'mergeColumnSelection' variable, you can ignore that but I'm including it in case it somehow ends up being relevant):
let renderedEnrichedDataFields = [];
for(let i = 0; i < this.state.enrichedData.length; i++) {
let mergeColumnSelection = ""
if(this.state.enrichedData[i] !== null) {
mergeColumnSelection = <div className="form-item__select form-item">
<label className="form-label" htmlFor="add-target-owner">Merging Column</label>
<div className="form-item__container">
<select onChange={(e) => {this.setEnrichedMergeColumn(e, i)}} defaultValue={this.state.csvColumns[0]}>
{mainDataColumns}
</select>
</div>
</div>
}
renderedEnrichedDataFields.push(
<div className="form-group">
<button onClick={() => {this.removeEnrichmentData(i)}} type="button" className="modal-close">
<Icon name="close" />
</button>
<div className="form-item">
<label className="form-label" htmlFor="add-target-csv">Enrichment Dataset</label>
<input
className="csv-input"
type="file"
accept="text/csv"
onChange={(e) => {this.acceptNewEnrichedDataFile(e, i)}}
/>
</div>
{mergeColumnSelection}
</div>
)
}
Basically, every time the button is pressed a new element is pushed into the enrichedData array in state. This causes the application to render an additional file input. When a user uploads a file, the placeholder element in the array is replaced with the file. When the user eventually submits the form, an array of files will be submitted which is great.
However! I'm having a lot of trouble getting a clean implementation for the ability to REMOVE these input fields. The function
removeEnrichmentData(index) {
let enrichmentData = this.state.enrichedData
let enrichedMergeColumns = this.state.enrichedMergeColumns;
enrichmentData.splice(index, 1);
enrichedMergeColumns.splice(index, 1)
this.setState({enrichedData: enrichmentData, enrichedMergeColumns: enrichedMergeColumns});
}
As you can see this takes the index of the selected input, then splices it from the array that generates the for loop. The appropriate file is spliced from the array, and the number of file inputs is correct. However, there are problems with what file name is displayed. Pictures will help:
Here you can see a sample where someone is preparing to upload three
files, health, cluster, and starbucks
Now I select to remove the cluster item (item 2) from the list. It is
removed from the file list in state, leaving just health and
starbucks. However, the for loop simply runs through twice and drops
the last item - meaning that it appears that the health and cluster
are the remaining two files, even though in actuality they are health
and starbucks
I thought about moving the JSX block itself into state so I can specifically target the JSX input element I want removed - but have had limited success with this approach and read that it's not advisable to put JSX into the state. React doesn't really have built in ways to easily delete the specific input, and I can't set default values in file inputs so I can't easily tie the individual inputs to their counterpart in state.
It feels like it should be such a simple problem and I'm very stuck. Any help is appreciated!
#Miloš Rašić is correct - your initial problem is that you're probably using array indices for the keys for the inputs. So, if you have 10 inputs numbered 0...9, and you delete the input with index 5, you're still rendering items with keys 0..8, and React thinks the last one was removed.
Per your comment about using UUIDs, it sounds like you're generating unique IDs in the render() method itself. DO NOT DO THAT! Never generate random values for keys in render(). When you do that, you're telling React every time that "this item is different than the last time we rendered, please destroy the existing item here and replace it with a new one".
Instead, you should generate these unique IDs when you add a new entry into your state. For example:
class FileInputList extends Component {
state = { inputs : [] }
addNewFileInput = () => {
const inputID = uuid();
const newInputs = this.state.inputs.concat({id : inputID});
this.setState({inputs : newInputs});
}
render() {
const {inputs} = this.state;
const inputList = inputs.map(inputEntry) => {
return <input type="file" key={inputEntry.id} />
});
return inputList;
}
}
It's hard to be sure without a fully working example, but this awfully similar to a "working" example of how React messes up when you don't give arrays of elements a key prop. Don't you get warnings about this from React? Try giving the divs you are pushing to the array a key prop which won't change for an existing element when an element is deleted (so key={i} won't work).
For example, if you are rendering
<input type="file" key={1} />
<input type="file" key={2} />
<input type="file" key={3} />
<input type="file" key={4} />
when you delete the one with key={2} it should be
<input type="file" key={1} />
<input type="file" key={3} />
<input type="file" key={4} />
Some sort of incrementing id like in relational databases or a generated unique id would do the trick.
Have you tried this:
{this.removeEnrichmentData(index)}}
Here is a working example.

ngModel cannot detect array changes correctly

The component model:
private SomeArray = [{ key: "Initial" }];
User can add/remove items dynamically:
addField() {
this.SomeArray.push({ key: Math.random().toString() });
}
removeField(index: number) {
this.SomeArray.splice(index, 1);
}
Template markup:
<div class="col-xs-12">
<button (click)="addField()" type="button">Add</button>
</div>
<div *ngFor="let field of SomeArray; let i = index;">
<input [(ngModel)]="field.key" #modelField="ngModel" [name]=" 'SomeArray['+i+'].key' " type="text" class="form-control" required />
<div [hidden]="modelField.pristine || !(modelField.errors && modelField.errors.required)" class="alert alert-danger">
Required error
</div>
<button (click)="removeField(i)" class="btn btn-danger">Remove</button>
</div>
This works untill user removes any item from SomeArray. If I add some two items initially:
and remove the one with 1 index:
then after adding another item Angular treat it as item has both 0 and 1 index (the new item "occupies" both two inputs):
(item with key 0.1345... is not displayed)
It's worth to noting items of SomeArray are as expected, but data binding fails. What can be the reason of it?
Update: Thanks to the comments of #Stefan Svrkota and #AJT_82 it's known for me the issue can be resolved by adding [ngModelOptions]="{standalone: true}" to the needed input. But I couldn't stop thinking about the reason of the issue in my cause, without setting standalone option (there is unique value for each name attribute so it's excepted nothing wrong here).
Finally I have found that behavior occurs when input elements are into <form> tag only - Plunker sample here (enclosing of template with form tag is the reason that issue).
Any ideas of this behavior?
The reason why it happens is ngFor mixes name properties when you delete some item.
When you use ngModel inside form each ngModel control will be added to form controls collection.
Let's see what happens if we have added three items and clicked on Remove the second
1) Step1 - SomeArray[1].key exists in collection controls
2) Step2 - SomeArray[1].key has been removed from controls collection
3) Step3 - Html looks like
4) Step4 We are adding a new item
So formGroup returns existing item.
How we can solve it?
1) Don't wrap our controls in form tag
2) Add ngNoForm attribute to form
<form ngNoForm>
3) Use
[ngModelOptions]="{standalone: true}
With all three solutions above:
We can remove [name] property binding
We can't use the built in Form group validation
4) Use trackBy for ngFor
template.html
<div *ngFor="let field of SomeArray; let i = index; trackBy: trackByFn">
component.ts
trackByFn(i: number) {
return i;
}
Plunker Example
This way our built in form will work properly

How to save checkbox selection having same name and id

I am looking out for logic how to create relationship between child element selected with its parent to differentiate on server to know that option is selected under particular parent.
e.g. In my following question i wanted to know that Math is selected under which parent (Andy, John or Murray) on server side.
I have the following code
<li ng-repeat="student in Students">
<input type="checkbox" class="checkbox" id="{{student[0].Id + $index + $parent.$index}}"/>
<label for="{{student[0].Id+ $index + $parent.$index}}"><span class="Radiobox-txt" id="{{student[0].Name}}">{{student[0].Name}}</span></label>
<ul>
<li ng-repeat="subject in student.Subjects">
<input type="checkbox" id="{{subject.Id+ $index + $parent.$index}}" ng-model="subject.Checked" />
<label for="{{subject.Id+ $index + $parent.$index}}"><span id="{{subject.Name}}">{{subject.Name}}</span></label>
</li>
</ul>
</li>
The above code generates UI like following with checkbox option showing with each:
Andy
Math
English
Computer
John
Math
English
Computer
Murray
Math
English
Computer
Currently, the following is my code where i am pushing selected child's Id into array.
Probably i need to modify this code to also push parent Id along with child Id so on server i can know the relationship but not sure whether to concatenate parentId or some other way around. Idea please?
$scope.studentDetail.PermissionIds = [];
angular.forEach($scope.permissions, function (p) {
var selectedPermissions = $filter('filter')(p.Children, { Checked: true });
for (var i in selectedPermissions) {
$scope.studentDetail.PermissionIds.push(selectedPermissions[i].Id);
Use student.id rather than student [0].id. I'm guessing that is unique?

Categories