Stackblitz
I have the following model
export class Items {
items: Map<string, string>;
createdBy: string;
deliveredBy: string;
}
I want to dynamically create input fields based on items in an array which I am able to I am unable to figure out how to populate data in those fields.
<form>
<tr>
<td *ngFor="let item of myStringArray">
Map {{item}}<input name="value" ([ngModel])="data.items.set(item)" (ngModelChange)="onChange()">
</td>
<td>
Always Present One <input ([ngModel])="data.createdBy" (ngModelChange)="onChange()" />
</td>
<td>
Always Present Two<input ([ngModel])="data.deliveredBy" (ngModelChange)="onChange()" />
</td>
</tr>
</form>
{{ data.items | json }}
Components.ts
export class AppComponent {
public myStringArray = ["First", "Second", "Third"];
data = new Items();
onChange() {
console.log(this.data);
}
}
I found a reference but I am unsure where I am going wrong
Reference Stackblitz
Thank you for your time and help
There are multiple errors in your Stackblitz:
You cannot directly bind to a Map within an ngModel. You need to get in the ngModel and set in the ngModelChange:
<td *ngFor="let item of myStringArray">
Map {{item}}<input name="value" [ngModel]="data.items.get(item)"
(ngModelChange)="data.items.set(item, $event)">
</td>
The syntax for the two way binding is "banana in a box" [(...)] not the opposite:
Always Present One <input [(ngModel)]="data.createdBy" />
Are per the console error "If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions." you need to either set ngModelOptions or add a name field:
Always Present One <input [(ngModel)]="data.createdBy" name="createdBy" (ngModelChange)="onChange()" />
Here is a new Stackblitz fixed.
Related
I am using angular6 reactive form with form builder and form array. I am facing problem with duplicate subject entry from drop down in form array. How to validate to avoid duplicate entry in from array.
I have a subject list with drop down. When i click on add button then a subject array will add. If i add similar subject it also be added. But i want to avoid duplicate subject entry. When i entry duplicate subject then a validation message will show and save button will disable.
stackblitz
ts code
olevelSubForm = this.fb.group({
olevelSubArray: this.fb.array([
])
});
olevelSubjectList: any = [ 'Geography','Mathematics',
'Physics','Chemistry'];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.addItemOlevel();
}
// olevel
createOlevelSub(): FormGroup {
return this.fb.group( {
olevelSubject: new FormControl('', Validators.compose(
[
Validators.required
]
)),
});
}
addItemOlevel() {
const control = <FormArray>this.olevelSubForm.controls.olevelSubArray;
control.push(this.createOlevelSub());
}
saveData() {
console.log('saved')
}
html code
<form [formGroup]="olevelSubForm" >
<div formArrayName="olevelSubArray">
<table>
<thead>
<tr style="font-size: 15px;">
<th>Subject</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of olevelSubForm.get('olevelSubArray').controls; let i = index;" [formGroupName]="i">
<td>
<select formControlName="olevelSubject">
<option *ngFor="let olevelSub of olevelSubjectList" [value]="olevelSub">{{ olevelSub }}</option>
</select>
</td>
<td>
<button style="float: right" [disabled]="olevelSubForm.invalid"(click)="addItemOlevel()"> Add
</button>
</td>
</tr>
</tbody>
</table>
<button [disabled]="olevelSubForm.invalid"(click)=saveData()>Save</button>
<pre> {{ olevelSubForm.value | json }} </pre>
</div>
In my comment I put and answer with a different aproach: that the options you can select was different in each level, so you can not choose some yet choosed. that, if you has a function like
getSubjectForFormArray(i, olevelSubjectList) {
return i == 0 ? olevelSubjectList :
this.getSubjectForFormArray(i - 1, olevelSubjectList.filter(x =>
x != this.olevelSubForm.get('olevelSubArray').value[i-1].olevelSubject))
}
Yes is a recursive function that begins with all the options and in each step, remove from the list the value we are choose. So, your option list can be like
<option *ngFor="let olevelSub of getSubjectForFormArray(i, olevelSubjectList)"
[value]="olevelSub">{{ olevelSub }}</option>
Anyway, this dont' avoid that if, e.g we choose "Mathematics","Chemistry", if change the first combo to Chemistry, we'll get two "Chemistry". So, we are going to check each change of the array. That's in ngOnInit
this.olevelSubForm.get('olevelSubArray').valueChanges.subscribe(res=>{
res.forEach((x,index)=>{
//res is the value of the array,e.g. -in one moment-
//[{olevelSubject:'Mathematics'},{olevelSubject:'Chemistry'},{olevelSubject:null}]
if (index)
{
//before becomes an array with the values selected before, eg. -in one moment-
// ['Mathematics','Chemistry']
const before=res.slice(0,index).map(x=>x.olevelSubject)
if (before.find(x=>x==res[index].olevelSubject))
{
(this.olevelSubForm.get('olevelSubArray') as FormArray)
.at(index).setValue({olevelSubject:null})
}
}
})
})
See the stackblitz
Preface:
I am having the hardest time trying to figure out what sounds like an easy process for nested angular forms. I am dealing with a few components here and some of the formGroups and formArrays are being dynamically created and its throwing me off.
Apologies for the large code dump, but its the minimal example I was able to come up with to try and explain my problem.
The parent component is very straight forward as it only has two formControls. I then pass the form to the tasks component to have access to it.
Parent Component
this.intakeForm = this.fb.group({
requestor: ['', Validators.required],
requestJustification: ['', Validators.required]
});
HTML:
<form [formGroup]=“intakeForm”>
<app-tasks
[uiOptions]="uiOptions"
[intakeForm]="intakeForm">
</app-tasks>
</form>
Tasks Component
Some method in here will trigger generateTask which creates the new form group.
ngOnInit() {
this.intakeForm.addControl('tasks', new FormArray([]));
}
// Push a new form group to our tasks array
generateTask(user, tool) {
const control = <FormArray>this.intakeForm.controls['tasks'];
control.push(this.newTaskControl(user, tool))
}
// Return a form group
newTaskControl(user, tool) {
return this.fb.group({
User: user,
Tool: tool,
Roles: this.fb.array([])
})
}
HTML:
<table class="table table-condensed smallText" *ngIf="intakeForm.controls['tasks'].length">
<thead>
<tr>
<th>Role(s)</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i">
</tr>
</tbody>
</table>
TR Component
Some method in here will trigger the addRole method which will add the form group.
#Input('taskTR') row;
#Input('ui') ui;
#Input('intakeForm') intakeForm: FormGroup;
// Add a new role
addRole($event, task) {
let t = task.get('Roles').controls as FormArray;
t.push(this.newRoleControl($event))
}
// Return a form group
newRoleControl(role) {
return this.fb.group({
Role: [role, Validators.required],
Action: [null, Validators.required]
})
}
HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
The Question
I need to add formControlName to my TR Component, specifically on the ng-select. However, when I try and add a formControlName, it tells me that it needs to be within a formGroup.
From what I can tell, the formGroup is in the tasksComponent and is wrapping the whole table so its technically within a formGroup?
My end goal is to be able to add the formControlName to this input but I am having a hard time trying to figure out the path to get there.
Here is an image of the full form object.
The last expanded section, Role, is what this input should be called via formControlName so that I can perform validation and what not on the control.
Updates
Edit 1 - Changes for #Harry Ninh
Tasks Component HTML
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i" [formGroup]="intakeForm"></tr>
</tbody>
TR Component HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
formControlName="Roles"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
Result: ERROR Error: formControlName must be used with a parent formGroup directive.
You are expected to declare [formGroup]="intakeForm" in the root tag of every component that wraps all formControlName, formGroupName and formArrayName properties. Angular won't try to go up the hierarchy to find that when compiling the code.
In TR Component template, the root element (ie. the <td>) should have [formGroup]="intakeForm", in order to tell Angular the formControlName who is related to.
I have a component which has a data table which I filter using a pipe,
The way I trigger and sent new argument to the pipe is on input-event on a input tag , I capture the input in 'targetInput' variable,
The above setup works, here is how it looks like:
<tr >
<td *ngFor="let column of currentView.columns">
<div *ngIf="column.label">
<input placeholder="{{column.label}}" id="{{column._devName}}" type="text"
(input)="targetInput = {targetValue:$event.target.value,targetId:$event.target.id,currentFilterMap:currentFilterMap}">
</div>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I decide , that I want a separate component for the input tag , so I split the html and make a parent-child setup and pass the shared variable using #Input decorator,
This is how the new setup looks ,
Parent html:
<tr >
<td *ngFor="let column of currentView.columns">
<filterTagBox [taskCol] = "column" [currentFilterMap] = "currentFilterMap"></filterTagBox>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I can't seem to pass the targetInput from the child component back to the parent on the input event, Not sure if this is the way I should implement this or if there is a better way.
I think in your case parent is Parent html and child is filterTagBox. if you want transfer value from parent to child you need use #input
if you want transfer value from child to parent you need use EventEmitter and #Output
more info.
https://angular.io/docs/ts/latest/cookbook/component-communication.html
I use BehaviorSubject to notify any component (the parent in your situation) that subscribes it. It's a special type of observables. A message service can do it for you. Define a message model (you can even use a simple string if you prefer) and create a message service:
import {Observable, BehaviorSubject} from 'rxjs/Rx'; //
import {Message} from "../../models/message"; // Your model
... inside your message service class:
private _newMessage = new BehaviorSubject<Message>(new Message);
getMessage = this._currentUser.asObservable();
sendMessage(message: Message) { this._newMessage.next(message) }
In a component (e.g. in a parent), you can subscribe getMessage subject like this:
this.messageService.getMessage.subscribe(
message => {
// a message received, do whatever you want
if (message == "so important message")
this.list = newList;
// ... so on
});
This way, multiple components can subscribe to this BehaviorSubject, and any trigger in any component/service that uses sendMessage method can change these subscribed components immediately. For you, that can be a child component:
... you successfully made something in your
... child component, now use the trigger:
this.messageService.sendMessage(new Message("so important message", foo, bar));
Thanks for the answers , I did figure out how I could do this ,and although I found using behaviour service interesting I decided to use a output variable to sent data form the child component to the parent which would ultimatedly sent to the pipe,
Here is what I did :
Child component HTML:
<div *ngIf="taskCol.label">
<div id="{{taskCol._devName}}_tagBox"></div>
<input placeholder="{{taskCol.label}}" id="{{taskCol._devName}}" type="text"
<!-- Call childComponent.onInput passing event parameters -->
(input)="onInput({targetValue:$event.target.value,targetId:$event.target.id})">
</div>
Child component.ts :
#Component({
selector: 'filterTagBox',
template: require('./filterTagBox.component.html')
})
export class FilterTagBox{
private colValues:string[];
public containsQueries:boolean;
private regex:RegExp;
#Input() public taskCol:TaskColumn;
#Output() onItemInput = new EventEmitter<any>(); // bound event to the parent component
// constructor and other hidden methods...
onInput(targetInput : any){
this.onItemInput.emit(targetInput); //trigger onItemInput event on inputBox input
}
}
Parent component html :
<tr >
<td *ngFor="let column of currentView.columns">
<!-- Catch custom onItemInput event which was triggered in the child -->
<filterTagBox (onItemInput)="filterBoxPipeData = {targetValue:$event.targetValue,targetId:$event.targetId,currentFilterMap:currentFilterMap}" [taskCol] = "column" ></filterTagBox>
</td>
</tr>
<!--sent the data retrieve from the input i.e filterBoxPipeData to the pipe i.e tagBoxFilterPipe along with data to be filtered i.e currentView.task -->
<ng-container *ngFor="let task of (currentView.tasks | tagBoxFilterPipe:filterBoxPipeData); let i=index">
<tr>
<!--hidden html -->
</tr>
I'm trying to set a dynamic form in Angular2.
So, in my ngOnInit function, I made a Ajax request to get a JSON with form data.
Like this :
export class CustomerEditComponent{
private customer : Customer = new Customer();
private customerForm;
constructor(private _CustomersService: CustomersService, private _routeParams: RouteParams, private _fb: FormBuilder){
this.customerForm = _fb.group({
name: [],
job: [],
arrival_date: []
});
}
ngOnInit(){
let id = this._routeParams.get('id');
this._CustomersService.getById(id).subscribe(res => {
this.customer = res;
});
}
onSubmit(event){
console.log(event);
}
}
So, at the component construct, 'customer' is equals to a newest one. (all properties are empty). But just after, we set value to every properties.
No problem for that, my input has the correct values.
But, if I submit my form, the form value is equals to :
Object {name: null, job: null, arrival_date: null}
(But the form in the view is correctly populate).
Here my form (condensed) :
<form [ngFormModel]="customerForm" (ngSubmit)="onSubmit(customerForm.value)">
<input md-input [(value)]="customer.name">
<input md-input [(value)]="customer.job">
<input md-input type="date" [(value)]="customer.arrival_date">
<button type="submit">Save</button>
</form>
I use [(value)] cause ng2-material package. (I already try with ngControl).
I think my code is 'wrong' about this feature, but I dunno where.
Thanks !
EDIT :
I have found the answer !
With ng2-material, we need to set [(value)] and [(ngModel)] together on every input like this :
<form [ngFormModel]="customerForm" (ngSubmit)="onSubmit(customerForm)">
<input md-input [(value)]="customer.name" [(ngModel)]="customer.name">
<input md-input [(value)]="customer.job" [(ngModel)]="customer.job">
<input md-input type="date" [(value)]="customer.arrival_date" [(ngModel)]="customer.arrival_date">
<button type="submit">Save</button>
</form>
[(value)] is used by ng2-material to set the value 'on front'.
I think that the problem is that you didn't associate your form inputs with their controllers within the ngFormControl directive in your template. You should refactor that way:
<form [ngFormModel]="customerForm" (ngSubmit)="onSubmit(customerForm.value)">
<input md-input [(value)]="customer.name"
ngFormControl="name">
<input md-input [(value)]="customer.job"
ngFormControl="job">
<input md-input type="date" [(value)]="customer.arrival_date"
ngFormControl="arrival_date">
<button type="submit">Save</button>
</form>
See this link from ng2-material samples: https://github.com/justindujardin/ng2-material/blob/master/examples/components/input/form_builder.html
Otherwise why don't you use the customer object instead of the customerForm.value one?
Hope it helps you,
Thierry
I have loaded a JSON list into a table and I would like to parse 1 JSON result or multiple results into an object, so I can send it to the server.
My table looks like this so far:
HTML
<tr ng-repeat="t in student">
<td ng-model="herkanserNaam">{{ t.Name }}</td>
<td>{{ t.City }}</td>
<td>
<div class="checkbox" ng-click="laatzien(herkanserNaam, herkanserCheck)" ng-model="herkanserCheck">
<label>
<input type="checkbox">
</label>
</div>
</td>
</tr>
Controller
$scope.laatzien = function(name, active) {
var herkanser = [{
"name" : name,
"active" : false
}];
console.log(herkanser);
}
How would I be able to check one or multiple results and save the data(t.Name) into an object by using a checkbox? So far the function laatzien() is returning the empty values defined in herkanser.
The reason your laatzien method is failing is due to how you are using your directives. Let's work with the example you provided to get your laatzien method to fire.
HTML
<tr ng-repeat="student in students">
<td>{{ student.Name }}</td>
<td>{{ student.City }}</td>
<td>van</td>
<td>Huis</td>
<td>j.huis#student.han.nl</td>
<td>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="student.isActive" ng-change="laatzien(student)">
</label>
</div>
</td>
</tr>
Javascript
$scope.laatzien = function (student) {
var herkanser = [{
"name": student.name,
"active": student.isActive
}];
console.log(herkanser);
}
I have made some opinionated changes in your example for readability purposes, others were needed to get the directives to fire as expected. Below are the changes to your snippets.
Renamed the student array to students. This will require a change in your controller from $scope.student to $scope.students.
Renamed the t object to student.
Removed the ng-click directive from your div.
Added an ng-change directive on your checkbox. Now when you click the checkbox your laatzien method should fire.
Added an isActive property to your student. Inside of your laatzien method, you may now check the state of the checkbox. If the checkbox is checked, student.isActive = true. If the checkbox is not checked, student.isActive = false.
From your code, you seem to want to build the "list of checked students" and send that to the server. In other words, what you want, is to allow the user to check on multiple students and at the end collect everything that was checked and send it over to the server.
If that's the case then your strategy to put an ng-click over the checkbox is wrong.
What you need is to bind your checkbox to your $scope model. Such as this:
<input type="checkbox" ng-model="t.isChecked" ng-true-value="true" ng-false-value="false'">
When the user checks the checkbox for a student. Your model will automatically be updated.
To collect the data to send over the server you need to put an ng-click on a submit button. In the event handler, simply loop through every student in your $scope "students" model and only save the ones that have isChecked property to true to be sent over to the server.
Hope this helps!
You could make a function to push thet item into an obj like so...
$scope.students = [
{
"name":"John",
"city":"Boston"
},
{
"name":"Amy",
"city":"Dallas"
}
]
$scope.activeObj = [];
$scope.laatzien = function(obj) {
if($.inArray(obj, $scope.activeObj) == -1) {
$scope.activeObj.push(obj);
} else {
var index = $scope.activeObj.indexOf(obj);
$scope.activeObj.splice(index, 1);
}
}
http://jsfiddle.net/5fcnazb2/