Angular Form Array - Dynamically Add Value to Nested Form Group - javascript

I followed the this video to create a reactive form to input data into an Angular application I am working on. It does what I want for the most part, however, I have added an additional control("setNumber") to be added in the reactive form array, but instead of inputting a value through the input fields of "name" and "gender", to enter it into the form I would like the value to auto-populate to the getUserForm group/submittable form automatically based on the iteration of the component .
I would ideally like it to display next to name and gender as well as be placed within the form
I put the code on StackBlitz here, where I just have setNumber(core>service>exerciseInput.service.ts) as its own input field and it does indeed update the form to be submitted...but for some reason there I am getting a type error on stackblitz that I am not getting in VSC. But its the code I am using and it works fine on my machine.
Application view
Anyway from stackblitz I believe I should be able to use property binding somewhere to pass i as a value into the component(not just the view as shown in the span string interpolation) so that the form automatically populates i as the setNumber within the userArray, but I've had no luck in my attempts over the last few days.
The fix should really be something incredibly easy I'm overlooking in the following block of code in input-array.component.html but I just cant get it to work.
<div *ngFor="let u of userArray.controls; index as i">
<span>Set {{ i + 1 }}</span>
<app-input [inputFormGroup]="$any(u)"></app-input>
<button (click)="removeUser(i)">Delete</button>
</div>
I would be incredibly grateful for any help!
Thank you

I'm trying to solve your problem and one of the solution is patching your FormArray via pipe, but I think there you need to implement ControlValueAccessor interface and make communication between parent and child Forms without mutation. Pay attention to IndexedFormPipe
Stackblitz

In your stackblitz, the type errors were fixed by adding form1: FormGroup in InputArrayComponent (I had to add types elsewhere too). Also, there were errors with regards to importing scss files that weren't there.
In any case, I may have misunderstood your question, but if you simply want to pass the value of i+1 to the input component and set that value to the FormControl of "setNumber", you simply add an Input() value to InputComponent (I've called it index):
#Input() index: number;
Then in the InputArrayComponent template I pass i+1 to the inputComponent
<app-input [index]="i+1" [inputFormGroup]="$any(u)"></app-input>
And in ngOninit() for the inputArray I assign the value:
this.inputFormGroup.get('setNumber').setValue(this.index);
Here's the stackblitz:
https://stackblitz.com/edit/angular-ivy-2rwlwu?file=src/app/views/input/input.component.html
--- EDIT ---
Your comment made clear an issue with deleting users – the index doesn't update. To have the index update automatically as users are deleted, you need to have the logic be performed any time the input changes, which can be done with a set function:
#Input() set i(value: number) {
this.index = value;
this.inputFormGroup.get('setNumber')?.setValue(value);
};
The issue with this is that it overwrites any changes to setNumber that the user may have made. There are ways around it, but my gut tells me it doesn't make sense for this value to be edited anyway.
So you could simply replace the input with a label that holds the value of index. The value will still appear in the form group (and in the submitted result), but there's no way for the user to edit it.
I've forked the stackblitz again with the differences:
https://stackblitz.com/edit/angular-ivy-hhhz9m?file=src/app/views/input/input.component.html

Related

Can I affect properties of an Angular component based on change of other properties in a non-template-driven fashion?

I'm developing a form with Angular 6 and using some standart components, including ng-select (I'd say that's a requirement). The form is supposed to have rather complicated logic (some field values affect other values or suggestions that ng-select should show). I have a rather general question (hoping that there's an approach which I haven't just found yet), but to stay more specific, let's consider the following example:
I have 2 dictionaries (let's call them categories and items), each item being a "child" of a certain category
I have to let user select a category and an item from each dictionary, for that I have 2 fields like
<ng-select
name="category"
[items]="formSuggestions.categories$ | async"
bindLabel="name"
[(ngModel)]="formFields.category"
></ng-select>
<ng-select
name="item"
[items]="formSuggestions.items$ | async"
bindLabel="name"
[(ngModel)]="formFields.item"
></ng-select>
(in fact, they are wrapped into custom components which I omit for simplicity)
(here formSuggestions.items$ and formSuggestions.categories$ are observables that are filled with suggestions on server response; each item is actually an object having id, name and parentId)
what I need is: when a category is selected, suggestions for items are limited to those which are children of that category; when an item is selected, the category is set automatically
My question is: is there a way in Angular to "subscribe" to changes of one property in model (formFields.item) and apply it to others (formFields.category, formSuggestions.categories$) or the only way to deal with this is to set Outputs like (change) of each field?
The problem with that approach is the actual form is more complicated, for instance:
there's another interface that should be shown in a modal window, where user can choose category (and same for item), so there's multiple points which change the props
item selection should affect another ng-select's suggestions (for another field) and pre-fill some crud interface with default stuff for that item
by the way, I have to show only 10 suggestions each time (suggestion dictionaries are quite long) and there's no "limit" option in ng-select, so I have to affect suggestion list based on field value
...
so I really wonder if I can go less template-driven. Any suggestions, at least for the 2 selections case?
(change) is listening to the classical input change event (not Angular specific). See also MDN-Link
For all [(ngModel)] bound elements, you could also use (ngModelChange) to listen to changes. Thats more Angular style. And it gets even more interesting when you create your own "input" components with the ControlValueAccessor.
The problem in your example is, that you use the subscribed suggestionCategories directly. You could (theoreticly) do a "map" in the observable stream and filter out the unwanted values. But this would only work for each emited event of the observable.
So in your case i fear you have to subscribe to the source, store the result in a component local variable. You also copy the data in a second variable that you use to show the values on the UI.
And whenever the user selects a category, you take the original stored data, filter it and assign the filtered result to your second-variable.
HTML
<ng-select
name="category"
[items]="formSuggestions.categories$ | async"
bindLabel="name"
[(ngModel)]="formFields.category"
(ngModelChange)="filterCategorySugestions($selectedValue)"
></ng-select>
In Typescript you would then use the filterCategorySugestions Method to filter the data and write it into your second variable (mentioned above).
by the way, when filtering, you could afterwards apply a mylist.splice(10) (standard Array method) to limit your results to the first 10. But perhaps you should ensure the order first. :-)
I hope it helps a bit.
warm regards
Jan

Optionally pass data to child element via directive

I have a popup dialog box that I wish to display one of two slightly different views, dependant on whether I have data present or not.
Is there a way in which I can optionally pass in said data within the directives?
ie. I would like to use something like
<my-comp *ngIf="ifPopup" [data]="myData" [isNew]="isNew"></my-comp>
where the [data] may not always be present, ie. It may either be undefined or have actual data present.
I just want to avoid having to basically duplicate my component.
Update after confusion around my question
Basically I am adding a new record to my DB (imagine adding a new customer etc). My component will either be used to edit a record or create a new record. If I am editing a record, myData will be filled with this record. If I am creating a new record, myData will be undefined..
why don't you just use
<my-comp *ngIf="ifPopup && myData" [data]="myData"></my-comp>
in this way, If the myData is not present, your popup wont display
Or you can also use <ng-container> and wrap your component inside and and use ngIf for your data!

How to avoid reflection of data entered in popup to the table before it is saved using Angular2

I have an edit popup, when the popup opens and i edit, it is reflecting on the table. I must avoid the reflection, once i click on save button then only the edited part must be displayed on the table. I am able to do this only for one input, i am not getting how to carry out the same way for other 2 inputs.
//Ts
editTutorial(tutorial) {
this.editTutorials.show();
this.edit_tut = tutorial;
}
You are assigning tutorial value which you sent from table into edit_tut varable, which is working as two-way binding.
so, the data in the table is getting changed along with your input.
The solution can be changing the variable reference, you can do something like,
editTutorial(tutorial) {
this.editTutorials.show();
let tut = JSON.parse(JSON.stringify(tutorial))
this.edit_tut = tut;
}
This will change the value reference and will work for you.
You have to have two different properties on your component, referencing to two different object instances. Few things to do:
Step 1: on clicking "edit", make a copy of the table row (all props) and put them inot the modal. Keep the reference to, e.g. table row you're editing, or _id or something.
In your case, add a property to TutorialComponent called currentlyEditing: any. Then, modify your editTutorial method:
editTutorial(tutorial) {
this.editTutorials.show();
this.currentlyEditting = tutorial;
}
Step 2: editing those should not reflect on the table. Go on and edit your thing.
Step 3: upon saving, sync your changes back to the table, or rather, to the original data set that's being displayed in the table. That's why you needed the reference from step 1.
Now, it's not clear to me if your edit_tut component is the one that saves changes. But if it is, I think everything will work as is. If not, you'd have to, after saving and response of "success", go and find the original tutorial in the tutorials array, and replace it with the edited component.

Editing a delimited string using multiple input fields in AngularJS

I am trying to do the following quite unsuccessfully so far.
I have an string that is semicolon separated. Say a list of emails, so
'email1#example.com;email2#example.com;email3#example.com'
What I am trying to accomplish is split this string (using split(';')) into an array of strings or array of objects (to aid binding). Each of the items I would like to bind to different input elements. After editing I want to read the concatenated value again to send to my backend.
Problem is that when editing one of the split inputs, the original item value is not update (which makes sense as I am guessing the individual items are copies of parts of the original), but I am wondering if there is a way to do something like that.
Note that I want this to go both ways, so watching the individual inputs and updating the original one manually, would just fire an infinite loop of updates.
I have tried a few different ways, including creating an items property get/set using Object.defineProperty to read and right to the string (set was never fired).
take a look at this plnker
You can construct a temporary array on each field update in order to do the string replacement of the old segment with the new value. In order to tackle the lost focus problem you will have to use the ngReapeat's track by $index. The internal array will not be recreated unless you add the separator to your original string.
Here is the complete solution on Plunker
Your main issue is your ng-model attribute on your repeated input element. I would start with making use of ng-repeat's $index variable to properly bind in ng-model. In your original Plunker 'name' is NOT a scope property you can bind to, so this should be changed to ng-model="names[$index]"
Here is a Plunker to reflect this. I made quite a few changes for clarity and to have a working example.
NOTE: You will find that when editing fields directly bound to a repeater, every change will fire a $digest and your repeated <input> elements will refresh. So the next issue to solve is regaining focus to the element you are editing after this happens. There are many solutions to this, however, this should be answered in a different question.
Although binding to a string primitive is discouraged, you could try ng-list.
<form name="graddiv" ng-controller="Ctrl">
List: <input name="namesInput" ng-list ng-model="vm.names"/>
<ul>
<input ng-repeat="name in vm.names track by $index" ng-model="name" ng-change="updateMe($index, name)"/>
</ul>
You'll need both track by $index and an ng-change handler because of the primitive string binding.
function Ctrl($scope) {
$scope.vm = {}; // objref so we can retain names ref binding
$scope.vm.names = ['Christian', 'Jason Miller', 'Judy Dobry', 'Bijal Shah', 'Duyun Chen', 'Marvin Plettner', 'Sio Cheang', 'Patrick McMahon', 'Chuen Wing Chan'];
$scope.updateMe = function($index, value){
// ng quirk - unfortunately we need to create a new array instance to get the formatters to run
// see http://stackoverflow.com/questions/15590140/ng-list-input-not-updating-when-adding-items-to-array
$scope.vm.names[$index] = value; // unfortunately, this will regenerate the input
$scope.vm.names = angular.copy($scope.vm.names); // create a new array instance to run the ng-list formatters
};
}
Here's your updated plunkr

How to properly clean form with invalid input from AngularJS controller?

I have an AngularJS form that contains - among other fields - one of type url. The latter is important as this forces the corresponding input to be a valid URL.
Under certain conditions (for instance, a modal dialog with such a form is to be closed), I want to clear that form programmatically. For that purpose, I implemented method reset that basically clears the corresponding form model by setting $scope.formData = {}. Thus, it sets the form model to a new, blank object.
While that assignment clears all valid fields in the rendered HTML form, it does not clear invalid fields, like an invalid URL. For instance, if the user would provide invalid input ht://t/p as URL, that input would not be removed from the rendered form.
I think this is due to the fact that any invalid URL is not reflected by the model - such an invalid URL just wouldn't "make" it to the model because it does not pass validation in the NgModelController#$parsers array. Thus, in the model - there is no URL at all. Consequently, resetting the form model to {} cannot actually change the model's URL as it has not been set yet.
However, if method reset explicitly sets field $scope.formData.url = "", the invalid URL will be cleared properly (at least, the rendered form won't show it anymore). This is caused by the explicit change of the URL in the model. However, now, model variable formData.url contains the empty string (well, not surprisingly), while by using = {}, all fields would be undefined instead.
While assigning individual fields to "" works as workaround for simple forms, it quickly becomes cumbersome for more complex forms with many fields.
Thus, how could I programmatically reset the form efficiently and effectively - including all invalid input fields as well?
I created a Plunker at http://plnkr.co/c2Yhzs where you can examine and run a complete example showing the above effect.
Specify the type of your button as reset. That will not only call the ngClick function, it will also clear the content of the HTML form.
<button type="reset" ng-click="resetFormData()">Reset</button>
I think this solution is moderately elegant: your plnkr reviewed
The big difference is the initialization of your model object.
I think things gets messed up when a variable becomes undefined, it doesn't get updated anymore.. it should be connected (veeeery) deeply with how validation works (docs link)
Returning undefined in that case makes the model not get updated, i think this is exactly what happens behind the curtain
PS: you can recycle resetImplicitly for all your forms in the webapp :)
After trying several answers without success in similar questions, this worked for me.
In my controller:
$scope.cleanForm = function() {
$scope.myFormName.$rollbackViewValue();
};
Just call with some ng-click or any way you want.
Cheers
The Thing is tag is of type "url" which means
if user will enter specifically a valid url then only it will set values of model
If user will expicitly reset it which means setting model values to "" will again make textbox empty .
It is looking like it is setting the values but actually not ,so when you set its value to "" .Angular will set modal value to ""
Lets take another example : put replace "text" with "email"
<input type="email" ng-model="formData.name" />
<br />URL:
<input type="url" ng-model="formData.url" />
<br />
In above code If you will enter invalid email it will not set the values of email's model.
You probably need to make a copy of the model in its pristine state and set the model to pristine when you reset.
There's a good example here:
http://www.angularjshub.com/examples/forms/formreset/
The url form fields are passed into the model only if they are valid. Thus in case of an invlaid-url entry in the form, the scope variable is not assigned with the model and clearing the forms entry by assigning an empty object to the model will still persist the value at the UI front.
The best alternative to this is to assign the model associated with the form data with a null. A similar answer appears here:
https://stackoverflow.com/a/18874550/5065857
ng-click="formData={};"
just give like this ,
<button ng-click="formData={}">(1) Reset Full Data: formData = {}</button>
Reset your form data directly in ng-click itself.

Categories

Resources