Unraveling Angular 2 book, Chapter 1, Example 5 - javascript

The page shows a list of dives, it has an "add new dive", "clear dives" and a search box, which filters the displayed list as you type into it.
This is the template:
<div class="container-fluid">
<h1>My Latest Dives (Angular/TypeScript)</h1>
<div class="row">
<div class="col-sm-5">
<button class="btn btn-primary btn-lg"
[disabled]="!enableAdd()"
(click)="addDive()">
Add new dive
</button>
<button class="btn btn-danger btn-lg"
(click)="clearDives()">
Clear dives
</button>
</div>
<div class="col-sm-4 col-sm-offset-3">
<input #searchBox class="form-control input-lg"
placeholder="Search"
(keyup)="0" />
</div>
</div>
<div class="row">
<div class="col-sm-4"
*ngFor="let dive of dives | contentFilter:searchBox.value">
<h3>{{dive.site}}</h3>
<h4>{{dive.location}}</h4>
<h2>{{dive.depth}} feet | {{dive.time}} min</h2>
</div>
</div>
</div>
This is the component code:
import {Component} from '#angular/core';
#Component({
selector: 'divelog',
templateUrl: 'app/dive-log.template.html'
})
export class DiveLogComponent {
public dives = [];
private _index = 0;
private _stockDives = [
{
site: 'Abu Gotta Ramada',
location: 'Hurghada, Egypt',
depth: 72,
time: 54
},
{
site: 'Ponte Mahoon',
location: 'Maehbourg, Mauritius',
depth: 54,
time: 38
},
{
site: 'Molnar Cave',
location: 'Budapest, Hungary',
depth: 98,
time: 62
}];
public enableAdd() {
return this._index < this._stockDives.length;
}
public addDive() {
if (this.enableAdd()) {
this.dives.push(this._stockDives[this._index++]);
}
}
public clearDives() {
this.dives = [];
this._index = 0;
}
}
This is the filter code:
import {Pipe, PipeTransform} from '#angular/core';
#Pipe({name: 'contentFilter'})
export class ContentFilterPipe implements PipeTransform {
transform(value: any[], searchFor: string) : any[] {
if (!searchFor) return value;
searchFor = searchFor.toLowerCase();
return value.filter(dive =>
dive.site.toLowerCase().indexOf(searchFor) >= 0 ||
dive.location.toLowerCase().indexOf(searchFor) >= 0 ||
dive.depth.toString().indexOf(searchFor) >= 0 ||
dive.time.toString().indexOf(searchFor) >= 0);
}
}
The filter is getting invoked and the list is getting rerendered whenever I type into the search box, but not when I click "add" button. If I have something in the search box, "add" button does not result in the change of the dive list even though the content of the search box allows the new items to be displayed. How do I change the code so that clicking the "add" button would cause rerendering of the displayed list of dives?

You have a pure pipe so
its method transform will be executed only when it detects a pure
change to the input value.
For your case
*ngFor="let dive of dives | contentFilter:searchBox.value"
the input value will be dives and searchBox.value
According to the angular2 guide on pipes:
A pure change is either a change to a primitive input value
(String, Number, Boolean, Symbol) or a changed object reference (Date,
Array, Function, Object).
When a dive is added, the array reference (dives) isn't changed – hence transform method is not executed.
When something is typed into the filter input, searchBox.value does change - hence transform is executed.
So one of possibles solutions is to have always a new reference array each time a div is added:
Just replace:
this.dives.push(this._stockDives[this._index++]);
with:
this.dives = this.dives.concat(this._stockDives[this._index++]);
or:
this.dives = [...this.dives, this._stockDives[this._index++]];
Second way to do it working is use impure pipe:
#Pipe({name: 'contentFilter', pure: false })

Related

Angular pass checkbox value to custom pipe

I'm looping through the values and displaying checkboxes which I'm showing, now in addition there is an search box filter on the top of list of checkboxes basically to filter out checkbox list.
HTML
<input [(ngModel)]="searchText" class="lmn-input lmn-select-input-inner mt-input" placeholder="Search by key word" id="mngd-seg"/>
<div formArrayName="checkBoxValueList"
*ngFor="let dataConceptList of scopeSetDetails.controls['checkBoxValueList'].controls | searchFilter : searchText; let i = index">
<label class="lmn-selection-control">
<span class="lmn-checkbox">
<input class="lmn-control-input" type="checkbox" [formControlName]="i">
<i class="lmn-control-icon lmnicon lmnicon-check"></i>
</span>
<span>Data Concept </span>
<span style="padding-left: 150px">{{ checkBoxValueList[i].scopeSets }}</span>
</label>
</div>
Therefore I've created an custom pipe 'searchFilter' and using this pipe above in *ngFor
PIPE
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'searchFilter',
})
export class FilterPipe implements PipeTransform {
transform(items: any, searchText: string): any {
if (!items) return [];
if (!searchText) return items;
searchText = searchText.toLowerCase();
return items.filter((it) => {
return it.name.toLowerCase().includes(searchText);
});
}
}
In TS I'm initializing the checkboxes to false (with checkboxes, the Angular form control value is bound to the "checked" state (not the "value" attribute), so it should be either true or false) since it needs to be unchecked initially.
TS
const formControls = this.checkBoxValueList.map(
(control) => new FormControl(false)
);
const selectAllControl = new FormControl(false);
this.scopeSetDetails = this.fb.group({
search_text: this.fb.control(''),
checkBoxValueList: new FormArray(formControls, this.minSelectedScopeSet(1)),
selectAll: selectAllControl,
});
Now issue is in filter pipe I'm getting items as an array of FormControl whose value property is 'false'. I need the checkbox text somehow and initially all checkboxes should be unchecked. how can I achieve this?

How to access the properties of a formArray in HTML?

I'm trying to implement a reactive Angular form, but, I can't access the properties of the array on HTML, I never worked with reactive form, if anyone could guide me I would be grateful! I'm using Angular 10 and I have the following code:
TS
operationModel: IScriptOperationsModel;
formOperation: FormGroup;
constructor(
private fb: FormBuilder,
...
) {}
ngOnInit() {
this.operationModel = new IScriptOperationsModel();
this.operationModel.scriptOperationOrders = [];
this.buildForm(new IScriptOperationsModel());
this.scriptOperationsService.findScriptOperation(this.operationId).subscribe((operation) => {
this.operationModel = operation.data as IScriptOperationsModel; // api return
this.buildForm(this.operationModel); // I pass the return of the api to the form
});
}
buildForm(operation: IScriptOperationsModel) {
this.formOperation = this.fb.group({
descriptionCode: [operation.descriptionCode],
description: [operation.description],
workStations: this.fb.array([])
});
this.formOperation.setControl('workStations', this.fb.array(this.operationModel.scriptOperationOrders));
}
get workStations(): FormArray {
return this.formOperation.get('workStations') as FormArray;
}
HTML
<div
class="card"
[ngClass]="{'bg-principal': idx === 0, 'bg-alternative': idx !== 0}"
formArrayName="workStations"
*ngFor="let workstation of workStations.controls; index as idx"
>
<div class="card-body" [formGroupName]="idx">
<div class="form-row">
<div class="form-group col-md-1">
<label>Id Oper.</label>
<input
type="text"
name="idOperation"
class="form-control"
disabled
formControlName="rank" <!-- whatever with or without binding gives error -->
/>
</div>
<div class="form-group col-md-2">
<label>Time</label>
<input
type="time" class="form-control" name="defaultTime"
[formControlName]="defaultTime" <!-- whatever with or without binding gives error -->
/>
</div>
</div>
</div>
</div>
Models
export class IScriptOperationsModel extends Serializable {
public description: string;
public descriptionCode: string;
public scriptOperationOrders: IScriptOperationOrdersModel[]; // array which I need
}
export class IScriptOperationOrdersModel extends Serializable {
public rank: number;
public defaultTime: string;
public asset?: IAssetModel;
public provider?: IProviderModel;
}
error-handler.service.ts:87 Error: Cannot find control with path: 'workStations -> 0 -> rank' # undefined:undefined
NOTE: I already looked at some answers here on the site such as this, this and this , but none of them solved this problem!
your problem is here :
this.formOperation.setControl(
'workStations',
this.fb.array(this.operationModel.scriptOperationOrders) <== here the problem
);
you are passing an array of IScriptOperationOrdersModel instead of array of form group.
To make your code working, you have to loop on every element of this.operationModel.scriptOperationOrders array , and instanciate a new FormControl object then push it in the workStations form array.
To access its elements, you can use controls[indexOfGroup].rate
You can take a look at this simple example you will understand everything.

Show hide text box when specific option is selected from dropdown inside dynamic form Angular

I have a form where a user could add one/more div of Address on click of add button.
I want if user select options=5 from the dropdown, want to show and hide textbox in that particular address Div.
Component Code
get contactFormGroup() {
return this.form.get('Array') as FormArray;
}
ngOnInit() {
this.form= this.fb.group({
Array: this.fb.array([])
});
}
createContact(): FormGroup {
return this.fb.group({
ABC: [null, Validators.compose([Validators.required])],
Test: [null, Validators.compose([Validators.required])]
});
}
addContact() {
this.Group.push(this.createContact());
}
showValue(event) {
const selectedValue = event;
if (selectedValue === '5') {
this.showValuetxtbox = true;
} else {
this.showValuetxtbox = false;
}
}
As you are looping to add the divs, you could use a template reference variable on the drop down. e.g #select then refer to that in the *ngIf:
<form [formGroup]="addExpressionform">
<div formArrayName="expressionArray">
<div *ngFor="let item of contactFormGroup.controls; let i = index;" [formGroupName]="i">
<carbon-dropdown #select
(optionSelected)="showValue($event)"
[formControlN]="'UOM'"
[options]="listOptions" [formGroup]="item"
name="UOM"
>
</carbon-dropdown>
<carbon-text-input *ngIf="select.value == 5"
[formControlN]="'Value'"
[formGroup]="item"
name="Value"
>
</carbon-text-input>
<carbon-button type="primary" (click)="submit()" id="save-parameter">Save</carbon-button>
</div>
</div>
</form>
Simplified StackBlitz demo.
Take a look at this Stackblitz, it's referred to in the Angular docs and could serve as boilerplate to what you are trying to achieve.
You should isolate every possible type of question by creating a different class for each one, so you can shape the data and then use ngSwitch to dynamically create the HTML accordingly.
Question base class:
export class QuestionBase<T> {
controlType: string;
value: T;
key: string;
label: string;
// etc
constructor(options) {
// constructor logic
}
}
Some special class that inherents from base class
import { QuestionBase } from './question-base';
export class SpecialQuestion extends QuestionBase<string> {
controlType = 'specialQuestion';
type: string;
// special Question
specialValue: string;
constructor(options) {
super(options);
this.type = options['type'] || '';
}
}
Then, a question component:
<div [formGroup]="form">
<label>{{question.label}}</label>
<div [ngSwitch]="question.controlType">
// controls logic
<input *ngSwitchCase="'textbox'" >
<select *ngSwitchCase="'specialQuestion'"></select>
</div>
</div>
Then you throw this into a container component where you loop through the entire questions array.
This way your code will be future proof and reusable as you add/change functionality to your forms down the road. You won't have to create spaghetti to meet edge case requirements like an extra input field.

Display input box on condition in angular reactive form

I am making angular 6 application where i am using angular reactive form.
Html:
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" style="display: flex">
<select (ngModelChange)="changeAction($event)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
Ts:
form: FormGroup;
showLabel: boolean = false;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
}
addCreds() {
const creds = this.form.controls.credentials as FormArray;
creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
}
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
Working stackblitz: https://stackblitz.com/edit/angular-form-array-example-yksojj
In this given example there will be an add button, upon clicking that button you will get a select-box with values as set,wait,go and an input called name.. Upon click over add button the same row will be added and forms each object inside array.
Also you can see an if condition inside html for label,
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
The thing i am in the need was in the select box if i choose set or go then the label needs to displayed otherwise it should not be displayed for which i have written,
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
To be clear enough If the user clicks three times add button then the dropdown and name field alone needs to be displayed for three times whereas if the user selects the value from dropdown as set or go then the label input needs to be displayed to that particular row alone where the dropdown has the value set and go.. If the selection was wait then there should not be label box for the row which has dropdown value as wait.
Kindly help me to achieve the expected result..
If you add disabled:true property to formControl it will exclude fromControl from formGroup then you can enable formControl manually
creds.push(this.fb.group({
action: '',
name: '',
label: {disabled:true, value: ""}
}));
Then enable using enable method
changeAction(e,index) {
if(e === "set" || e === "go") {
this.showLabel = true;
this.form.get('credentials').at(index).get('label').enable();
} else {
this.showLabel = false;
this.form.get('credentials').at(index).get('label').disable();
}
}
Example:https://stackblitz.com/edit/angular-form-array-example-5buwyr
Please, NOT use (ngModelChange) or changeAction($event) to get the value of a control in an array -well, ngModelChange is for Template driven form, not for Reactive Form.
First change your form, create the form using a div with formArrayName="credentials", and a inner div *ngFor="let creds of form.get('credentials').controls
<!--a div with formArrayName--->
<div formArrayName="credentials" >
<!--a div *ngFor over "controls", in this div don't forget formGroupName-->
<div *ngFor="let creds of form.get('credentials').controls; let i = index"
[formGroupName]="i" style="display: flex">
<select formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}">
{{option.value}}
</option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="??????????????">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
Well, now the condition. To get the value of "action" you can use
form.get('credentials').at(i).value.action
//or, better
creds.value.action
So, your div becomes like
<div *ngIf="creds.value.action=='set' ||
creds.value.action=='go'">
<input placeholder="Label" formControlName="label">
</div>
This aproach avoid unnecesary code in your .ts.
this.showLabel
The scope of this variable is your whole component. Therefore, turning it on or off will show and hide all inputs.
You need a per-row value (creds.showLabel in your interface), or use this in your template :
*ngIf="['set','go'].includes(creds.action)"
Updated Stackblitz
By the way, this :
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
is more elegant written this way :
changeAction(e) {
this.showLabel = ['set','go'].includes(e)
}
or this way :
changeAction(e) {
this.showLabel = e in ['set','go']
}
Well, Jeremy's answer is pretty nice and enforces to use most of the native apis given by platform/language, however, here is a traditional approach to understand what is the actual flow and scope of objects, etc,etc...
Root Cause: The show hide field is made global to the scope of component, not at per formgroup level. So changing a single value, and using for all, will affect to all.
Solution:
Use Jeremy's answer for clean coding and less error prone code.
Manage an array of variable that will take care of sho/hide detail for each form group.
In the below answer, added some comments for easy understanding, and added console.log to see what exactly happening. Also, played with index i created in *ngFor and showed how you can make use of these things in future.
import { Component } from '#angular/core';
import { FormControl, FormGroup, FormArray, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: 'app.component.html',
})
export class AppComponent {
form: FormGroup ;
showLabel = [];
creds : FormArray;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
this.creds = this.form.controls.credentials as FormArray;
}
addCreds() {
this.creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
// every time adding new foem grp, adding lable show/hide data also.
this.showLabel.push(false);
}
changeAction(e, i) {
//every time input clikced, managing show/hide for that particular data also
console.log("iii", i, e.target.value);
if( e.target.value == "set" || e.target.value == "go") {
this.showLabel[i] = true;
} else {
this.showLabel[i] = false;
}
}
}
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" >
<select (change)="changeAction($event, i)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel[i]">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
See live stackblitz working code
Note: Traditional is meant here.. as we every time do i.e. handle our problem on our own and create new problem on us to solve. It's not a tradition. :P

Angular .removeAt(i) at FormArray does not update in DOM - Angular

I thought it was an issue with my implementation but seems that my code for creating dynamic FormArray should be functional based from this question I raised. When I integrate it to my project, the remove function does delete the element from the FormArray, but it does not reflect in the interface/ does not remove object from the DOM. What could be causing this?
import {
Component,
VERSION
} from '#angular/core';
import {
FormGroup,
FormControl,
FormArray,
Validators,
FormBuilder
} from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
objectProps: any[];
public dataObject = [{
"label": "Name",
"name": "name",
"type": "text",
"data": ""
},
{
"label": "Contacts",
"name": "contacts",
"type": "inputarray",
"array": []
}
]
form: FormGroup;
constructor(private _fb: FormBuilder) {}
ngOnInit() {
const formGroup = {};
for (let field of this.dataObject) {
if (field.type == "inputarray") {
console.log("THIS IS " + field.type)
formGroup[field.name] = this._fb.array([''])
} else {
console.log("THIS IS " + field.type)
formGroup[field.name] = new FormControl(field.data || '') //, this.mapValidators(field.validation));
}
}
this.form = new FormGroup(formGroup);
}
addFormInput(field) {
const form = new FormControl('');
( < FormArray > this.form.controls[field]).push(form);
}
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
}
}
<form *ngIf="form" novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<div *ngFor="let field of dataObject">
<h4>{{field.label}}</h4>
<div [ngSwitch]="field.type">
<input *ngSwitchCase="'text'" [formControlName]="field.name" [id]="field.name" [type]="field.type" class="form-control">
<div *ngSwitchCase="'inputarray'">
<div formArrayName="{{field.name}}" [id]="field.name">
<div *ngFor="let item of form.get(field.name).controls; let i = index;" class="array-line">
<div>
<input class="form-control" [formControlName]="i" [placeholder]="i">
</div>
<div>
<button id="btn-remove" type="button" class="btn" (click)="removeFormInput(field.name, i)">x</button>
</div>
</div>
</div>
<div>
<button id="btn-add" type="button" class="btn" (click)="addFormInput(field.name)">Add</button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger btn-block" style="float: right; width:180px" [disabled]="!form.valid">Save</button>
This is not a great solution but I solved my problem by manipulating value and after removing the control.
I simply moved the item that I wanted to remove to the end of the array and then I removed the last item.
removeItem(index: number): void {
const value = this.formArray.value;
this.formArray.setValue(
value.slice(0, index).concat(
value.slice(index + 1),
).concat(value[index]),
);
this.formArray.removeAt(value.length - 1);
}
I hope it helps someone struggling with this issue in the future.
Maybe you can try to force change detection using the reference to application. To do that inject the ApplicationRef in the constructor an call the tick(); on your removeFormInput method.
constructor(private _fb: FormBuilder, private appRef: ApplicationRef) {}
And in removeFormInput
removeFormInput(field, i) {
(<FormArray>this.form.controls[field]).removeAt(i);
this.appRef.tick();
}
Take a look at angular documentation: API > #angular/core /ApplicationRef.tick()
replace below function, you are not removing the row object from 'dataObject'.
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
this.dataObject.splice(this.dataObject.indexOf(field),1);
}
Take a look here Add and Remove form items I build on stackblitz, for me its working fine, adding and removing items... Take a look.
working version
I was facing this problem as well. The solution for me was to get rid of/fix the trackBy function in NgFor*. I think you need to introduce a proper trackBy function and it might solve your error.
sauce: How to use `trackBy` with `ngFor`

Categories

Resources