Trouble navigating an Angular form to access it's controls - javascript

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.

Related

Create forms according to the user action. But not at once

I have to build a form like so.
This shows upfront. i.e. without user interaction.
When a user pressed the + button it creates the same kind of UI again like so.
You can see that the user can add any number of same UI parts again and again. Can you tell me how to do this?
I went through number of articles. But it has the whole form created once. i.e. not like my use case. Any direction for this?
Example: https://jasonwatmore.com/post/2019/06/25/angular-8-dynamic-reactive-forms-example
and https://alligator.io/angular/reactive-forms-formarray-dynamic-fields/
Based on the form you need to reproduce :
Create a function returning a formGroup to populate your array
createSchoolPath(): FormGroup {
return this.fb.group({
schoolName: '',
level: '',
topics: [],
inProgress: true
})
}
You then need to create your parent form which include your form array (and populate it once for the first form you want to show upfront) :
constructor( private fb: formBuilder) {}
form: FormGroup = this.fb.group({
schoolPaths: this.fb.array([this.createSchoolPaths])
});
Finally as you want to let users add more sections, you need a way to populate your array :
/* component */
addSchoolPath(): void {
const paths = this.form.get('schoolPaths') as FormArray;
// use the first function to push a new formGroup
paths.push(this.createSchoolPath());
}
<!-- html -->
<button (click)="addSchoolPath()">+</button>
To display it :
<form [formGroup]="form">
<div formArrayName="schoolPaths" *ngFor="let path of form.get('schoolPaths').controls; let i = index">
<div [formGroupName]="i">
<!-- place your inputs here -->
</div>
</div>
</form>
<button (click)="addSchoolPath()">+</button>
To complementary Gérôme's answer, you can also use directly a formArray
createGroup()
{
return new FormGroup({
school:new FormControl(),
level:new FormControl(),
topic:new FormControl(),
progress:new FormControl()
})
}
At first you has a formArray
formArray:FormArray=new FormArray([this.createGroup()])
The add button is simple
add()
{
this.formArray.push(this.createGroup())
}
And the .html
<form [formGroup]="formArray">
<div *ngFor="let group of formArray.controls" [formGroup]="group">
<input formControlName="school">
<input formControlName="level">
<input formControlName="topic">
<input formControlName="progress">
</div>
</form>
See that, in general we use a formArray inside a formGroup, But in case you only want a FormArray, you can loop over the formArray.controls directly

How to validate duplicate entry in angular form builder array?

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

Vuejs bind class to row then later hide/show the row?

I'm trying to create a table where the user can hide rows, and keep them hidden as they delete other rows.
In my html I use vuejs to bind a class when rendering the table:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}">
There is a user checkbox to hide rows with that class:
<label><input type="checkbox" v-model="showBlankFruits" #change="setBlankDisplay">Show Blank Fruits</label>
In my Vue instance, the checkbox executes a method to hide/show rows with that class via jquery to attach the css display property:
methods: {
setBlankDisplay: function() {
if (this.showBlankFruits) {
$('.noFruits').css('display', '');
} else {
$('.noFruits').css('display', 'none');
}
},
In my jsfiddle, when a user deletes a row, the hidden row reappears. I see that attaching styles with jquery in this instance is not good... does anyone have a suggestion for a better method?
Mixing Vue and jQuery is not recommended, as you can do pretty much everything just using Vue and you don't get any conflicting operations that don't know what the other library/framework is doing.
The following will show the row if either the fruits array length is truthy, in other words not 0, or if showBlankFruits is true:
<tr v-for="item in mylist" v-show="item.fruits.length || showBlankFruits">
The following will toggle showBlankFruits when clicking the checkbox:
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
Full code example:
JSFiddle
You can also write something like this.
I've used computed and removed the jQuery part completely.
You must declare data as a function instead of an data object (https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function)
You do not need to call the mounted method to set the initial state. It's already set with your data object.
In your code, you have to call mounted, because jQuery can only hide the results, when the DOM is loaded.
new Vue({
el: '#app',
data() {
return {
showBlankFruits: true,
mylist: [
{'user': 'Alice', 'fruits': [{'name': 'apple'}]},
{'user': 'Bob', 'fruits': [{'name': 'orange'}]},
{'user': 'Charlie', 'fruits': []},
{'user': 'Denise', 'fruits': [{'name': 'apple'}, {'name': 'orange'}]},
]
}
},
computed: {
list() {
return this.mylist.filter(item => (item.fruits.length > 0 && !this.showBlankFruits) || (item.fruits.length === 0 && this.showBlankFruits))
},
},
methods: {
delItem(item) {
let index = this.mylist.indexOf(item);
if (index > -1) {
this.mylist.splice(index, 1);
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div id="app">
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
<br> <br>
<table>
<tr> <th class="col">User</th> <th class="col">Fruits</th> <th class="col"></th> </tr>
<tr v-for="item in list">
<td>{{ item.user }}</td>
<td> <span v-for="f in item.fruits"> {{ f.name }} </span> </td>
<td> <button #click="delItem(item)">Delete</button> </td>
</tr>
</table>
</div>
This is a great example of the power for declarative rendering rather than using DOM manipulation in components. The problem you are running into is when the Vue engine re-renders your list it doesn't know about the manual manipulation of elemets you did in the setBlankDisplay method. The way to get around that is to use component logic in the definition of the view itself, sort of like you did to set the noFruits class in the first place.
So, I propose you get rid of setBlankDisplay and replace it with the method:
itemDisplay(item) {
if (item.fruits.length === 0 && !this.showBlankFruits) {
return 'none';
}
return '';
},
Then you can reference it in the definition of your tr elements linked to a css display property, like so:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}" :style="{display: itemDisplay(item)}">
I've updated the jsfiddle with this modification, showing that the state of hidden fruits remains when other items are deleted.
Take this as a general example of the dangers of using jquery to change the state of the view. Every effort should be taken to define the entire view in terms of component logic.

Angular 4 nested form update

I've got the following 'triple level' nested form:
FormGroup->ArrayOfFormGroups->FormGroup
Top level (myForm):
this.fb.group({
name: '',
description: '',
questions: this.fb.array([])
});
Nested form array element for 'questions':
this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']})
});
Nested form group element for 'params' is a key:value object of random length.
I'm using the following ngFor to go through elements:
<tr *ngFor="let questionConfigForm of myForm.controls.questions.controls; let i=index" [formGroupName]="i">
...
<div *ngFor="let param of objectKeys(questionConfigForm.controls.params.controls)" formGroupName="params">
<input type="text" [formControlName]="param">
I've got the following behavior:
When I'm updating any of the fields on first two form levels I could instantly see changes in corresponding form controls values with {{myForm.value | json}}.
But if I input something in one of 'params' controls I couldn't see any changes in myForm values, but the form data for 'params' controls will be updated if I will make any changes in corresponding 'questions' form.
For me it looks like 'param' form control receives input data, but doesn't trigger some update event, and I don't know how to fix that, except writing my own function to react on (change) and patchValue in form..
So my question is how to make 'params' controls update myForm without that strange behavior?
UPD:
initQuestionConfig() {
return this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']}),
});
}
addQuestionConfig() {
const control = <FormArray>this.myForm.controls['questions'];
const newQuestionCfg = this.initQuestionConfig();
control.push(newQuestionCfg);
}
Finally the problem is solved.
The root of this issue was the way I've cleaned up already existing 'params'.
To remove all parameters from 'questions' I used the following code:
const control = <FormArray>this.myForm.controls['questions'];
control.controls[index]['controls'].params = this.fb.group([]);
And the reason of those glitches was this new 'fb.group' instance.
Now I'm removing params one by one, keeping original formGroup instance and it works as expected:
const control = <FormArray>this.myForm.controls['questions'];
const paramNames = Object.keys(control.controls[index]['controls'].params.controls);
for (let i = 0; i < paramNames.length; i++) {
control.controls[index]['controls'].params.removeControl(paramNames[i]);
}
#MilanRaval thanks for your time again :)
Try this: Give formArrayName and formGroupName like below...
<div formArrayName="testGroup">
<div *ngFor="let test of testGroup.controls; let i=index">
<div [formGroupName]="i">
<div class="well well-sm">
<label>
<input type="checkbox" formControlName="controlName" />
</div>
</div>
</div>
</div>

sharing event driven variable across component in angular 2

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>

Categories

Resources