Unable to render collection dynamically set at run time - javascript

I have a piece of JSON which represents some actions and parameters a user can set.
I want to display the actions in a dropdown, and when a user selects one - the required parameters associated with the action are displayed.
Example:
User selects an action with a single parameter, a single input box is displayed
User selects an action with two parameters, two input boxes are displayed.
I nearly have it working but the *ngFor isn't displaying the inputs for the selected action:
In onChange - if I print this.steps, I can see that this.steps[i].SelectedAction.UIParameters has a value so I'm not sure why it isn't being rendered.
JSON:
[
{
"ActionEnum": "CLICKELEMENT",
"Name": "Click Element",
"UIParameters": [
{
"ParameterEnum": "ELEMENTID",
"Description": "The id of the element to click"
}
]
},
{
"ActionEnum": "INPUTTEXT",
"Name": "Input Text",
"Description": "Enters text into the element identified by it's id",
"UIParameters": [
{
"ParameterEnum": "ELEMENTID",
"Description": "The id of the element"
},
{
"ParameterEnum": "TEXTVALUE",
"Description": "The text to enter into the element"
}
]
}
]
Typescript:
import { Component, Output, EventEmitter, OnInit } from "#angular/core";
import { ActionService } from "../services/action-service";
import { Action } from "../models/Action";
#Component({
selector: 'app-scenario-step-editor-component',
template: `
<form #formRef="ngForm">
<div *ngFor="let step of steps; let in=index" class="col-sm-3">
<div class="form-group">
<label class="sbw_light">Action:</label><br />
<select (change)='onChange()' [(ngModel)]="step.Action" name="action_name_{{in}}">
<option *ngFor="let action of this.availableActions" [(ngModel)]="steps[in].value" name="action_name_{{in}}" class="form-control" required>
{{action.Name}}
</option>
</select>
<div *ngIf="steps[in].SelectedAction">
<label class="sbw_light">Parameters:</label><br />
<ng-template *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-template>
</div>
</div>
</div>
<button id="addStepBtn" type="button" class="btn btn-light" (click)="addScenarioStep()">Add Scenario Step +</button>
</form>`
})
export class ScenarioStepEditorComponent implements OnInit {
#Output() onSelectValue = new EventEmitter<{stepInputs: any[]}>();
steps = [];
availableActions: Action[];
constructor(private actionService: ActionService) {}
ngOnInit(): void {
this.actionService.list().subscribe( result => {
this.availableActions = result;
},
error => console.log('Error getting actions...') );
}
/* When user picks an option, save the chosen action with the rest of the parameters*/
onChange() {
for (let i = 0; i < this.steps.length; i++) {
let actionIndex = this.availableActions.findIndex(a => a.Name === this.steps[i].Action);
this.steps[i].SelectedAction = this.availableActions[actionIndex];
}
this.onSelectValue.emit( {stepInputs: this.steps} );
}
addScenarioStep() {
this.steps.push({value: ''});
}
}

<ng-template *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-template>
Just replace ng-template with ng-container:
<ng-container *ngFor="let parameter of steps[in].SelectedAction.UIParameters">
<label class="sbw_light">{{parameter.ParameterEnum}}</label><br />
<input (change)='onChange()' type="text" [(ngModel)]="steps[in].Parameters" name="parameter_name_{{in}}" class="form-control" #name="ngModel" required />
</ng-container>
Reasons:
ng-container was suitable for that situation. It just holds "something" and can be iterated.
ng-template defines a template. You didn't need a template here, templates are not meant to be used for that. It could work, of course, but it's not suitable for your scenario.
Read more about ng-template and ng-container here: https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/
As a final side note, you could use ng-template by defining an item and you could use ng-container with *ngTemplateOutlet to render the template. Check the above guide for some examples.

Related

How to generate dynamic tab with data based on API in Angular8

I have below JSON response.
From JSON response i am creating dynamic tab and inside every tab i want to push formArray based on below mentioned condition.
**In below response,
const myObj = [
{
'TabName': 'Test1',
'otherDetails': [
{
'processTechType': 'Continuous'
},
{
'processTechType': 'Batch',
},
]
},
{
'TabName': 'Test2',
'otherDetails': [
{
'processTechType': 'Batch'
}
]
}
];
For Ex - TabName Test1 and TabName Test2 are tabs name which i am displaying dynamically. Now in Test1 Tab, i want to push formArray Continuous and formArray Batch both forms. Because in Test1 Tab, i have processTechType array with Continuous and batch both. So it will show both form in Test1 Tab.
Ex - 2 -- Now in Test2 Tab, i want to push formArray Batch form only. Becuase in Test2 Tab, i have processTechType batch in otherDetails object. So it will show Batch form only in Test2 Tab.
My mean to say is everytime it will check the Tabname and otherDetails key from response and show forms based on processTechType array key on specific tab only.
I have below code. But it is pushing both forms in all tabs, not on specific tab. For ex - From my code, It is showing Continuous formArray onetime and Batch formArray two times in Test1 and Test2 tabs both.
Expected output -
In Test1 Tab, it will push one Continuous and one Batch form.
In Test2 Tab, It will show push batch form only.
Can anyone please help me get my code work to get my expected output.
getMakeLineData() {
var otherDetails = myObj.filter(m => m.otherDetails).map(m => m.otherDetails);
this.makeLineData = myObj;
if (otherDetails) {
otherDetails.forEach(element => {
for (var i of element) {
if (i.processTechType === 'Continuous') {
this.addQuantity();
}
else if (i.processTechType === 'Batch') {
this.addBatch();
}
}
});
}
}
createContinuousForm() {
return this.fb.group({
designProcess: ['', [Validators.required]]
});
}
createBatchForm() {
return this.fb.group({
avgBCT: ['', [Validators.required]]
});
}
continuousType(): FormArray {
return this.dataCollectionForm.get("continuousType") as FormArray;
}
batchType(): FormArray {
return this.dataCollectionForm.get("batchType") as FormArray;
}
addQuantity() {
this.continuousType().push(this.createContinuousForm());
}
addBatch() {
this.batchType().push(this.createBatchForm());
}
HTML Template
<div class="tabGroupDiv row">
<div class="lossLinesDiv">
<mat-tab-group class="lossMatGrpCls" mat-align-tabs="left">
<mat-tab *ngFor="let lineData of makeLineData">
<ng-template mat-tab-label>
<button class="validatorTabBgClr">{{lineData.makeLineName}}</button>
</ng-template>
<form [formGroup]="dataCollectionForm" (ngSubmit)="onSubmit()">
<!-- <--continuous Form start here -->
<div class="admin-console-main-wrapper" formArrayName="continuousType">
<div class="content-wrapper" *ngFor="let lineItem of continuousType().controls; let i=index"
[formGroupName]="i">
<div class="row list-wrapper">
<div class="col-xs-3 col-md-3 deo-dv-list-wrapper">
<h5 class="topbar-items-text">Design Process Capacity (Tonnes)</h5>
<mat-form-field appearance="outline">
<input matInput type="text" class="line-fte-input smed-input" placeholder="Design Process Capacity"
formControlName="designProcess">
</mat-form-field>
<mat-error *ngIf="lineItem?.controls?.designProcess?.hasError('required')">
Field is required
</mat-error>
</div>
</div>
</div>
</div>
<!-- <--continuous Form start here -->
<!-- <--Batch Form start here -->
<div class="admin-console-main-wrapper" formArrayName="batchType">
<div class="content-wrapper" *ngFor="let lineBatchItem of batchType().controls; let i=index"
[formGroupName]="i">
<div class="row list-wrapper">
<div class="col-xs-3 col-md-3 deo-dv-list-wrapper">
<h5 class="topbar-items-text">Average BCT (mins)</h5>
<mat-form-field appearance="outline">
<input matInput type="text" class="line-fte-input smed-input" placeholder="Average BCT"
formControlName="avgBCT">
</mat-form-field>
</div>
</div>
</div>
</div>
<!-- <--Batch Form ends here -->
</form>
</mat-tab>
</mat-tab-group>
</div>
</div>
Your object is complex to relationa tabs and FormArrays, So, imagine you has a more confortable object. Some like
[{tabName:'Tab 1',continuousType:0,batchType:0},
{tabName:'Tab 2',continuousType:1,batchType:-1}]
See that you use "-1" to indicate the tab has no a form of this type
For this create a function thah return this more confortable array
tabsConfig(obj:any){
const tab=[];
let continuousType=0;
let batchType=0;
obj.forEach((x,index)=>{
tab[index]={tabName:x.TabName,continuousType:-1,batchType:-1}
x.otherDetails.forEach(y=>{
if (y.processTechType=='Continuous')
tab[index].continuousType=continuousType++
if (y.processTechType=='Batch')
tab[index].batchType=batchType++
})
})
return tab
}
Now you can use a variable to store the config
this.tabs=this.tabConfig(this.myObj)
You can then
this.tabs.forEach(x=>{
if (x.continuousType!=-1)
this.continuousType.push(this.createContinuousForm())
if (x.batchType!=-1)
this.batchType.push(this.createBatchForm())
})
And, if you has two auxiliars forms that return the formGroup of the of the FormArrays
getContinuousForm(index:number){
return (this.dataCollectionForm.get("continuousType") as FormArray)
.at(index) as FormGroup
}
getBatchForm(index:number){
return (this.dataCollectionForm.get("batchType") as FormArray)
.at(index) as FormGroup
}
You're ready to create the mat-tab iterating over the "tabs" array
<mat-tab-group >
<mat-tab *ngFor="let tab of tabs;let i=index">
<ng-template mat-tab-label>
<button >{{tab.tabName}}</button>
</ng-template>
<form *ngIf="tab.continuousType!=-1
[formGroup]="getContinuousForm(tab.continuousType)">
....
</form>
<form *ngIf="tab.batchType!=-1
[formGroup]="getBatchForm(tab.batchType)">
....
</form>
</mat-tab>
</mat-tab-group>

Duplicating forms with logic

I have created a dynamic form field that I add and remove through a callback function on buttons DelBtn() to remove item and AddBtn() to add item
Each of this form fields have a value input
I also have a totalval field. I expect the sum of values in all the value field not to exceed the totalval. If it does we display an a error message and if the value equals we make the reason form field appear.
Example:
If I have totalValue = 100 . Now lets say I have my first form field value = 90.
Then I duplicate the form and in the next field set value = 10 then the reason field should appear because 90 + 10 = 100.
As the totalValue has been reached the reason form field should appear and the add button should be disabled.
If in the second attempt if the user tries to enter a value more than 10 then an error message should be shown.
Below is my current code
In my TS File I have
ischecks: boolean = true;
formsArray = [""];
count: number = 0;
totalval: number = 100;
ngOnInit(): void {}
constructor() {}
clickCount(): void {
this.count++;
}
DelBtn = delIndex => this.formsArray.splice(delIndex, 1);
AddBtn = () => this.formsArray.push("");
HTML
<h2> Form</h2>
<pre style="font-weight: bolder;font-family:verdana;margin-left: 35px;">Total Value:{{totalval}} </pre>
<div *ngFor="let i of formsArray; let a = index">
<div>
<form>
<table>
<div>
<label for="fname">Value:</label><br>
<input type="text" id="fname" name="fname" ><br>
<tr>
<td>
<button type="button" class="btn btn-outline-success"
style="border-radius:40px;margin-left: 50px" (click)="DelBtn(a)" ><span class="fa fa-plus"></span>Delete</button>
</td>
</tr>
<tr>
</div>
</table>
</form>
</div>
</div>
<div *ngIf=ischecks style="margin-left:35%">
<label for="fname">Reason:</label><br>
<input type="text" id="fname" name="fname" ><br>
</div>
<br>
<button type="button" class="btn btn-outline-success"
style="border-radius:40px;margin-left: 50px;margin-bottom: 30%;" (click)="AddBtn()" ><span class="fa fa-plus"></span>Add</button>
https://stackblitz.com/edit/angular-ivy-3pbdwv?file=src%2Fapp%2Fapp.component.html
Note: If you do not understand the question feel free to ask me in comments adn I am working in angular(typescript)
This will be difficult to achieve with basic Javascript. Below approach uses ReactiveForm approach
We Follow the below steps
Add the ReactiveFormsModule to the imports array of the module
#NgModule({
imports:[ ReactiveFormsModule, ... ],
Inject the FormBuilder class
constructor(private fb: FormBuilder) {}
Define the form
myForm = this.fb.group({
totalVal: [100],
formsArray: this.fb.array([this.fb.control("", { validators: [Validators.required] })]),
reason: ["", [Validators.required]]
}, { validators: [sumMatches] });
We have added a cusom validator sumMatches. We will use this to check whether the sum of the total value has been matched
function sumMatches(control): ValidationErrors | undefined {
const totalVal = Number(control.get("totalVal").value);
const formsArrayTotal = control
.get("formsArray")
.value.reduce((a, b) => Number(a) + Number(b), 0);
if (formsArrayTotal !== totalVal) {
return {
sumMismatch: true
};
}
return;
}
Next we define helper getter functions to extract properties from the formGroup
get sumMismatch(): boolean {
return this.myForm.hasError('sumMismatch')
}
get arrayFullyFilled() {
return !this.formsArray.controls.some(item => item.errors)
}
get formsArray() {
return this.myForm.get("formsArray") as FormArray;
}
get totalVal() {
return this.myForm.get("totalVal") as FormControl;
}
We also need to amend the functions to add and remove items from the formArray
DelBtn = delIndex => this.formsArray.controls.splice(delIndex, 1);
AddBtn = () => this.formsArray.push(this.fb.control(""));
Finally we can implement the formGroup in the html
<h2> Form</h2>
<span class='totalVal'>Total Value:{{ totalVal.value }}</span>
<form [formGroup]='myForm'>
<ng-container formArrayName='formsArray'>
<table *ngFor="let item of formsArray.controls; let i = index">
<tr>
<td>
<div>
<label [attr.for]="'fname' + i">Value:</label><br>
<input type="number" [formControlName]="i" type="text" [id]="'fname' + i" name="fname" ><br>
</div>
</td>
<td>
<button type="button" class="btn btn-outline-success"
s (click)="DelBtn(i)" ><span class="fa fa-plus"></span>Delete</button></td>
<tr>
</table>
</ng-container>
<div *ngIf='!sumMismatch && arrayFullyFilled'>
<label for="fname">Reason:</label><br>
<input type="text" id="fname" name="fname" ><br>
</div>
<br>
<button type="button" class="btn btn-outline-success"
(click)="AddBtn()" ><span class="fa fa-plus"></span>Add</button>
<br>
<span class="error" *ngIf="sumMismatch && myForm.touched">Total Value Mismatch</span>
</form>
I have extracted css to own file
.totalVal {
font-weight: bolder;
font-family: verdana;
}
.btn-outline-success {
border-radius: 40px;
margin-left: 50px;
}
.error {
color: red;
}
See this Demo
Edit 1 - How Does the validator work?
To understand this we look at how we build our form group. We defined a structure that produces a value in the form
{
totalVal: 100,
formsArray: [''],
reason: ''
}
By defining our form group as this.fb.group({ ... }, {validators: [ sumMatches ] } the form group with the above value will be passed to the sumMatches function
In the sumMatches we will have a something like a formGroup with the value
{
totalVal: 100,
formsArray: ['50', '20', '10'],
reason: ''
}
In the above we simply extract the 100 from the formGroup using control.get('totalVal').value same to formArray. Since formArray value will be an array then we can use reduce function to sum this.. We finally compare this and return null if they match and an Object if they do not match.
With the above approach, angular reactive forms will update the value of the form valid status based on what is provided by the user. We can hence leverage this valid status to update the UI
arrayFullyFilled()
get arrayFullyFilled() {
return !this.formsArray.controls.some(item => item.errors)
}
The above code tries to find if the user has filled ALL the inputs in the array. In our array we get all the controls, check if some of them have errors and if any has error return false otherwise return true. This is made possible considering that in my formGroup I had made the formControls as required using Validators.required validation

How to add multiple selected checkbox values to an array using Javascript?

I have an input element being populated using the *ngFor loop fetching the data from another array. On selecting multiple checkboxes, I need their values to be pushed into my empty array 'selectedArr'.
Find below the code:
import { Component } from "#angular/core";
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
title = "CodeSandbox";
toDo = ["Test", "Eat", "Sleep"];
task: string;
addTask(task: string) {
this.toDo.push(task);
}
selectedArr = [];
deleteValue() {}
addSelected(i) {
let checkId = document.getElementsByClassName("i");
console.log(checkId);
if (checkId.checked === true) {
this.selectedArr.push(i);
}
console.log(this.selectedArr);
}
}
<div>
<div class="form-group">
<label>Add a Task: </label>
<input class="form-control" type="text" [(ngModel)]="task" />
</div>
<button (click)="addTask(task)">Add</button>
<br />
<br />
<div>
My To Do List:
<ul>
<li *ngFor="let todo of toDo, index as i">
<input class="i" type="checkbox" (click)="addSelected(i)" />
{{todo}}
</li>
</ul>
</div>
<div class="btn class">
<button class="btn btn-primary" (click)="deleteValue()">Delete</button>
</div>
</div>
Try like this:
.html
<li *ngFor="let todo of toDo, index as i">
<input class="i" type="checkbox" [(ngModel)]="checked[i]" (ngModelChange)="addSelected(todo,$event)" />
{{todo}}
</li>
.ts
checked = []
selectedArr = [];
addSelected(item,evt) {
if (evt) {
this.selectedArr.push(item);
}else {
let i = this.selectedArr.indexOf(item)
this.selectedArr.splice(i,1)
}
}
Working Demo
The getElementsByClassName method of Document interface returns an array-like object of all child elements which have all of the given class name(s). Since you are passing the index, you can access the clicked element like:
addSelected(i) {
let checkId = document.getElementsByClassName("i")[i];
console.log(checkId);
if (checkId.checked) {
this.selectedArr.push(i);
} else {
// Remove the index from selectedArr if checkbox was unchecked
let idx = this.selectedArr.indexOf(i)
if (idx > -1) this.selectedArr.splice(idx, 1)
}
console.log(this.selectedArr);
}
please, the things easy works easy. You needn't actually manually the array. You should use a function (*)
get selectedArray()
{
return this.toDo.filter((x,index)=>this.checked[index])
}
<li *ngFor="let todo of toDo, index as i">
<!--remove (ngModelChange) -->
<input class="i" type="checkbox" [(ngModel)]="checked[i]" />
{{todo}}
</li>
{{selectedArray}}
(*) this allow you "start" the app with some selected

add custom validation in input type text using angular material stepper

I am using angular stepper to display all my data and take input for some of the text boxes, but I want to add my custom validations when user clicks on Next button.
stackblitz - material-stepper-custom-validation
html:
<mat-horizontal-stepper #stepper>
<mat-step>
<div class="m-10">
<input type="text" id="fname" placeholder="First Name" >
</div>
<div class="m-10">
<input type="text" id="lname" placeholder="Last Name" >
</div>
<div>
<button mat-button (click)="checkData()" matStepperNext>Next</button>
</div>
</mat-step>
<mat-step [stepControl]="secondFormGroup" [optional]="isOptional">
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
You are now done.
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button (click)="stepper.reset()">Reset</button>
</div>
</mat-step>
</mat-horizontal-stepper>
Here, first name and last name should be validated before go to next stepper, but not using formGroup.
ts:
import {Component, OnInit} from '#angular/core';
import {FormBuilder, FormGroup, Validators} from '#angular/forms';
#Component({
selector: 'stepper-optional-example',
templateUrl: 'stepper-optional-example.html',
styleUrls: ['stepper-optional-example.css']
})
export class StepperOptionalExample implements OnInit {
constructor() {}
ngOnInit() { }
checkData() {
let lname = (<HTMLInputElement>document.getElementById("fname")).value;
if(lname == '') {
alert('error');
}
return false;
}
}
How? - If first name is empty then don't allow them to go Next stepper.
Because you use the template driven approach, you will need to map all input fields into an ngModel somehow. Here is an example for it:
HTML:
<input type="text" required [(ngModel)]="model.name" name="name">
TS:
#Component({
selector: 'stepper-optional-example',
templateUrl: 'stepper-optional-example.html',
styleUrls: ['stepper-optional-example.css']
})
export class StepperOptionalExample implements OnInit {
#ViewChild('stepper') stepper;
model = {
name: 'Initial Value'
}
constructor() {}
ngOnInit() { }
}
Using that you can then, check the attribute onClick. You need to remove the matStepperNext and add the (click) event listener instead like so:
HTML:
<button mat-button (click)="onNext()">Next</button>
TS:
onNext() {
// Validate your value in the function
if (this.model.name !== 'Henry') {
this.stepper.next();
}
}
Other than that I also recommend to take a look on the official guide showing how to implement the template driven approach: https://angular.io/guide/forms

How to select button in angular

How to select button in angular
Here is my code from template.
ngOnInit() {
}
<div class="form-group">
<ng-container *ngFor="let lib of library; let i=index">
<div class="radio">
<input type="radio" id="library-{{i}}" name="lib-books"
(change)="selectOnChange($event)"
[value]="lib">
<label for="library-{{i}}"></label>
</div>
<label for="library-{{i}}">{{lib}}</label>
</ng-container>
</div>
Create a property isSelected to your lib object and
Sets up two-way data binding to your radio button:
[(value)]="lib.isSelected"
Equivalent to:
(valueChange)="lib.isSelected=$event"
You should use a Form component, either template driven or reactive
Template driven: You use two-way-binding for ngModel to store the selected value in your form model:
export class AppComponent {
library:string[] = [
'lib1', 'lib2', 'lib3'
];
// note: you should create an interface for that
model = {
books: '',
}
onSubmit () {
// save the selection
}
}
The difference to your code: the two-way binding to the model field [(ngModel)]="model.books"
<div class="form-group" onsubmit="onSubmit()">
<ng-container *ngFor="let lib of library; let i=index">
<div class="radio">
<input type="radio" id="library-{{i}}" name="lib-books"
[(ngModel)]="model.books"
[value]="lib">
<label for="library-{{i}}"></label>
</div>
<label for="library-{{i}}">{{lib}}</label>
</ng-container>
</div>
And don't forget to import the forms module in app.module.ts
import { FormsModule } from '#angular/forms';
#NgModule({
imports: [ FormsModule ],
})
Check out the StackBlitz for the template driven solution
Reactive forms (my personal preference, especially with the form builder):
export class AppComponent {
library: string[] = [
'lib1', 'lib2', 'lib3'
];
myForm: FormGroup;
constructor(private __fb: FormBuilder) {
this.createForm();
}
createForm() {
this.myForm = this.__fb.group({
book: ''
});
}
onSubmit() {
// save the selection
}
}
Note: the name attribute of the <input> needs to be the same as the formControlName and the <form> tag needs a [formGroup] binding.
<form [formGroup]="myForm" onsubmit="onSubmit()">
<div class="form-group">
<ng-container *ngFor="let lib of library; let i=index">
<div class="radio">
<input type="radio"
[value]="lib"
name="book"
formControlName="book"
id="library-{{i}}"
>
</div>
<label for="library-{{i}}">{{lib}}</label>
</ng-container>
</div>
</form>
Check out the StackBlitz for the reactive solution

Categories