Angular: Making Button Toggle Group required for Material Stepper - javascript

I'm relatively new to Angular and am just getting to grips with the Material Design Components and how they work and can interact with each other.
I'm building a multi-step web application, so it seemed logical to use the Material Stepper. The user will also be able to choose between various options on these steps, so I've used the Button Toggle Group to present these options.
What I'd like to be able to do - and am wondering if it is possible - is to make the Button Toggle Group a "required" field for the stepper. On some instances, the toggle group will be the only option on the step and I'd like to make it so that the user can't move onto the next step without selecting a Toggle Group option.
I've tried simple solutions like adding "required" onto the mat-button-toggle, like so:
<mat-button-toggle (click)="getOptionName(Oname);" class="btn btn-primary step-button" id="{{step1option.id}}" [ngClass]="status ? 'selected' : ''" required><span #Oname>{{step1option.text}}</span></mat-button-toggle>
And onto the mat-button-toggle-group:
<mat-button-toggle-group required>
But, unfortunately it's not that simple, which I didn't expect it to be. But hey, you might as well start with the most simple and obvious solution first, right?
Obviously, it didn't work and my knowledge of Angular isn't that extensive at the moment and trying various searches didn't turn up anything helpful (and/or anything I could understand).
Here's my code so far:
My component.html:
<mat-horizontal-stepper [linear]="isLinear" #stepper>
<mat-step label="Step 1" [stepControl]="firstFormGroup">
<form [formGroup]="firstFormGroup">
<div ng-controller="step1">
<h2>I'm thinking of...</h2>
<mat-button-toggle-group>
<div *ngFor="let step1option of step1options">
<mat-button-toggle (click)="getOptionName(Oname);" class="btn btn-primary step-button" id="{{step1option.id}}" [ngClass]="status ? 'selected' : ''"><span #Oname>{{step1option.text}}</span></mat-button-toggle>
</div>
</mat-button-toggle-group>
<div>You chose <strong>{{ selectedStep1Option }}!</strong></div>
<button mat-stroked-button matStepperNext class="btn btn-secondary continue-btn">Continue</button>
</div>
</form>
</mat-step>
<mat-step label="Step 2" [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<p>Test</p>
</form>
</mat-step>
<mat-step label="Step 3" [stepControl]="thirdFormGroup">
<form [formGroup]="thirdFormGroup">
<p>Test</p>
</form>
</mat-step>
</mat-horizontal-stepper>
And my component.ts:
import { Component, OnInit } from '#angular/core';
import { TimeoutError } from 'rxjs';
import {FormBuilder, FormGroup, Validators} from '#angular/forms';
#Component({
selector: 'app-customer-portal-step1',
templateUrl: './customer-portal-step1.component.html',
styleUrls: ['./customer-portal-step1.component.scss']
})
export class CustomerPortalStepOneComponent implements OnInit {
isLinear = true;
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;
thirdFormGroup: FormGroup;
constructor(private _formBuilder: FormBuilder) { }
ngOnInit() {
this.firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required]
});
this.secondFormGroup = this._formBuilder.group({
secondCtrl: ['', Validators.required]
});
this.thirdFormGroup = this._formBuilder.group({
secondCtrl: ['', Validators.required]
});
}
selectedStep1Option: string = 'nothing yet';
step1options = [
{
text: 'Buying my first home',
id: 'buying'
},
{
text: 'Moving to a new home',
id: 'moving'
},
{
text: 'Remortgaging my home',
id: 'remortgage'
},
{
text: 'Purchasing a buy-to-let',
id: 'purchase'
}
];
status: boolean = false;
getOptionName (Oname: any) {
this.selectedStep1Option = Oname.textContent;
localStorage.setItem('I\'m thinking of:', JSON.stringify({ motivation: Oname.textContent }));
}
}
I guess what I'm asking is, is this even possible, am I on the right track and, if not, what's the best approach for me to get the result I'm hoping for?
Any help you can offer would be appreciated.

You can use attribute disabled along with a dynamic expression to disable the matStepperNext, preventing the user from moving forward in the stepper. It looks like your default value is 'nothing yet'. You can disable the button for example by checking if the value is still the default/invalid 'nothing yet':
<mat-button-toggle-group>
<div *ngFor="let step1option of step1options">
<mat-button-toggle (click)="getOptionName(Oname);" class="btn btn-primary step-button" id="{{step1option.id}}" [ngClass]="status ? 'selected' : ''"><span #Oname>{{step1option.text}}</span></mat-button-toggle>
</div>
</mat-button-toggle-group>
<div>You chose <strong>{{ selectedStep1Option }}!</strong></div>
<button mat-stroked-button matStepperNext class="btn btn-secondary continue-btn" [disabled]="selectedStep1Option === 'nothing yet'">Continue</button>
Here is an example in action showing disabling a button based on a mat-button-toggle-group value.
Hopefully that helps!

Related

How to bind to an Angular form from users selected option

OK it's a bit more complicated than the headline..
This form I am working on is a form group. It has a few fields ( supplement name, description and tags) the supplement name one is what I need help with as I have not worked on a complicated form like this and want to get it right and not just offer a messy patch job.
Here is the expected logical order of what happens
user adds a new supplement by clicking on the field and begins typing "creatine" for example
there is a query sent out that fetches products based on the entry into the input and
returns a JSON that are offered as suggestions
user clicks the suggestion "creatine"
field is populated and binds
we add another entry through the "add option" and repeat for X amount of products we want to
add.
What actually happens
user adds new supplement by clicking the field and types "creatine" suggestion request is
sent off and populates the dropdown
user clicks on the suggestion "creatine" the field takes that value
value is actually blank
user adds another supplement but the previous selection is in the field
user clears it and types again
value is blank
What needs to happen is the user can add X amount of supplements and able to grab whatever option from the dropdown recommendation and it is added to the form group array and does not interfere with the other form group array values.
I know this is not the right way to bind the form and I don't think it's right the way i'm binding the mat input field to trigger the query and this is the reason why I'm asking the question again, to not offer a patch job.
Component code
import { Subscription } from 'rxjs/Subscription';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { UtilitiesService } from '../../utilities/utilities.service';
import { GetSupplementsService } from './get-supplements.service';
#Component({
selector: 'app-supplements',
templateUrl: './supplements.component.html',
styleUrls: ['./supplements.component.css'],
providers: [GetSupplementsService],
})
export class SupplementsComponent implements OnInit {
supplementForm: FormGroup;
queryField: FormControl = new FormControl();
private supplementInventoryResults: Array<ISupplementInventoryResponse>;
private eventForm: FormGroup;
private searchResults: any;
private searchSubscription: Subscription;
private addSupplementSubscription: Subscription;
subcription: Subscription;
constructor (
private bottomSheet: MatBottomSheet,
private _fb: FormBuilder,
private ref: ChangeDetectorRef,
private _utils: UtilitiesService,
private getSupplements: GetSupplementsService,
private router: Router
) { }
public ngOnInit(): void {
this.browsingStackHistory = false;
this.loading = true;
this.supplementForm = this._fb.group({ // the form in question
entryArray: this._fb.array([
this.getUnit()
])
});
this.searchSubscription =
this.queryField.valueChanges
.debounceTime(600)
.distinctUntilChanged()
.switchMap((query) => this.getSupplements.search_supplement_by_category(query))
.subscribe((result) => {
this.searchResults = result;
});
}
public ngOnDestroy(): void {
this.subcription.unsubscribe();
}
private getUnit(): FormGroup {
return this._fb.group({
supplementName: [''],
description: [''],
tags: ['']
});
}
private addUnit(): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.push(this.getUnit());
}
private removeUnit(i: number): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.removeAt(i);
}
private addSupplement(): void { // this will do the post to the service
const supplementObject = {
start: this._utils.get_upload_time(),
data: this.supplementForm.value,
rating: 0
};
}
}
Template
[![<mat-tab label="Add Stack (Test)">
<div style="padding:8px;">
<div fxLayout="row wrap">
<div fxFlex.gt-sm="50%" fxFlex="100">
<h1>Add New Supplements Stack</h1>
<form \[formGroup\]="supplementForm" class="log-workout">
<!-- Start form units array with first row must and dynamically add more -->
<div fxLayout="column" fxLayoutAlign="center center" class="row-height">
<div formArrayName="entryArray">
<mat-divider></mat-divider>
<!-- loop throught units -->
<div *ngFor="let reps of supplementForm.controls.entryArray.controls; let i=index">
<!-- row divider show for every nex row exclude if first row -->
<mat-divider *ngIf="supplementForm.controls.entryArray.controls.length > 1 && i > 0"></mat-divider>
<br>
<!-- group name in this case row index -->
<div \[formGroupName\]="i">
<!-- unit name input field -->
<div class="row">
<mat-form-field class="example-form">
<input matInput placeholder="Supplement Name" \[formControl\]="addSupplementField"
formControlName="supplementName" \[matAutocomplete\]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let product of supplementResults" \[value\]="product?.product_name">
<img class="example-option-img" aria-hidden \[src\]="product?.product_image" height="25">
{{product?.product_name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Description" formControlName="description" required>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Tags" formControlName="tags" required>
</mat-form-field>
</div>
<!-- row delete button, hidden if there is just one row -->
<button mat-mini-fab color="warn" *ngIf="supplementForm.controls.entryArray.controls.length > 1"
(click)="removeUnit(i)">
<mat-icon>delete forever</mat-icon>
</button>
</div>
</div>
<!-- New unit button -->
<mat-divider></mat-divider>
<mat-card-actions>
<button mat-raised-button (click)="addUnit()">
<mat-icon>add box</mat-icon>
Add Other Product
</button>
</mat-card-actions>
<button mat-raised-button (click)="addSupplement()">
<mat-icon>add box</mat-icon>
Add Supplement
</button>
</div>
</div>
<!-- End form units array -->
</form>
</div>
</div>
</div>][1]][1]
Having the below when the getUnit() function is called apparently binds it in the sense it will operate independently and without conflicts.
private getUnit(): FormGroup {
const formGroup = this._fb.group({
supplementName: [''],
review: [''],
rating: [''],
notes: [''],
tags: ['']
});
formGroup.get('supplementName').valueChanges
.debounceTime(300)
.distinctUntilChanged()
.switchMap((search) => this.getSupplements.search_supplement_by_category(search))
.subscribe((products) => {
this.supplementResults = products;
});
return formGroup;
}

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`

Add inputs dynamically when click on button in angular 4

I would create a form with the possibility to add inputs dynamically
I found a question about the same problem in angular 2 but I can't make it working in my exemple
Here's my component ts file :
export class AjoutProjetComponent implements OnInit {
isLinear = false;
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;
constructor(private _formBuilder: FormBuilder) {}
ngOnInit() {
this.secondFormGroup = this._formBuilder.group({
pers: [this._formBuilder.array([this.createItem()])]
});
}
createItem(): FormGroup {
return this._formBuilder.group({
name: ['', Validators.required]
poste: ['', Validators.required],
});
}
addItem(): void {
const control = < FormArray > this.secondFormGroup.controls['pers'];
control.push(this.createItem());
}
}
then HTML file
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>Constituez votre équipe</ng-template>
<div formArrayName="pers">
<mat-form-field *ngFor="let control of secondFormGroup.controls.pers.controls; let i= index">
<input matInput placeholder="Nom collaborateur" formControlName="name" required>
</mat-form-field>
</div>
</form>
</mat-step>
<div>{{secondFormGroup.value | json}}</div>
When I click in my favorite icon I get this error :
ERROR TypeError: control.push is not a function at AjoutProjetComponent.addItem
How can I make adding dynamically inputs working ?
UPDATE
I have updated my html code so that I could print two inputs but when I run my code I get this error now
ERROR Error: Cannot find control with path: 'pers -> name'
You did not declare your FormArray properly. You use arrays only to initialize simple FormControls, not FormGroups or FormControls, change to :
this.secondFormGroup = this._formBuilder.group({
pers: this._formBuilder.array([this.createItem()]) // remove opening and closing brackets
});
To see the inputs added dynamically to the html, you need to use an ngFor loop. I think you somewhat misunderstood the usage of formArrayName, which only adds context to the template to use with FormArrays. Try this:
<ng-container formArrayName="pers">
<input placeholder="Address"
*ngFor="let control of secondFormGroup.controls.pers.controls"
[formControl]="control.controls.name" required />
</ng-container>
And read more about FormArrayName directive here

Unraveling Angular 2 book, Chapter 1, Example 5

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 })

Angular 2 Dynamic Form - Implement Clear Functionality

I'm trying to implement Clear functionality for Angualr 2 dynamic forms, which should clear all the values to null. But i'm getting an error when i try to use this.questions[i].value="null"; or this.questions[i].value="undefinded"; or this.questions[i].value=''; to replace each value in the form.
Working Code: http://plnkr.co/edit/SL949g1hQQrnRUr1XXqt?p=preview
Typescript class code:
import { Component, Input, OnInit } from '#angular/core';
import { FormGroup, REACTIVE_FORM_DIRECTIVES } from '#angular/forms';
import { QuestionBase } from './question-base';
import { QuestionControlService } from './question-control.service';
#Component({
selector: 'dynamic-form',
templateUrl: 'app/dynamic-form.component.html',
directives: [REACTIVE_FORM_DIRECTIVES],
providers: [QuestionControlService]
})
export class DynamicFormComponent implements OnInit {
#Input() questions: QuestionBase<any>[] = [];
form: FormGroup;
payLoad:object;
questiont: QuestionBase<any>;
constructor(private qcs: QuestionControlService) { }
ngOnInit() {
this.form = this.qcs.toFormGroup(this.questions);
console.log("Form Init",this.questions);
this.questiont = JSON.parse(JSON.stringify(this.questions));
}
onSubmit() {
this.payLoad = JSON.stringify(this.form.value);
this.payLoad2=this.payLoad;
this.questiont = JSON.parse(JSON.stringify(this.questions));
}
cancel(){
console.log("Canceled");
this.questions = JSON.parse(JSON.stringify(this.questiont));
}
clear(){
for(var i=0;i<this.questions.length;i++){
this.questions[i].value='';
}
console.log("Cleared");
}
}
HTML code:
<div>
<form [formGroup]="form">
<div *ngFor="let question of questions" class="form-row">
<label [attr.for]="question.key">{{question.label}}</label>
<div [ngSwitch]="question.controlType">
<input *ngSwitchCase="'textbox'" [formControlName]="question.key"
[id]="question.key" [type]="question.type" [(ngModel)]="question.value">
<select [id]="question.key" [(ngModel)]="question.value" *ngSwitchCase="'dropdown'" [formControlName]="question.key" >
<option *ngFor="let opt of question.options" [ngValue]="opt.key" >{{opt.value}}</option>
</select>
</div>
<div class="errorMessage" *ngIf="!form.controls[question.key].valid">{{question.label}} is required</div>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid" (click)="onSubmit()">Save</button>
<button type="button" class="btn btn-default" (click)="cancel()">Cancel</button>
<button type="button" class="btn btn-default" (click)="clear()">Clear</button>
</div>
</form>
<div *ngIf="payLoad" class="form-row">
<strong>Saved the following values</strong><br>{{payLoad}}
</div>
</div>
Is there any way that can be done without getting error.
The problem is with your logic to check and display a message if a field is required.
Your plunker code was throwing the exception Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'
The problem is that it runs over your template code with values for questions[i].value but then it changes once you call clear. Angular compares questions[i].value and notices it's different then when it started rendering and thinks it made a mistake. It didn't pick up that you changed the value yet. You have to let it know the value was changed.
If you were to run the code with prod enabled, it wouldn't run this safety check and you'd get no error, but don't enable prod mode just to fix this.
Solution
The fix is import ChangeDetectorRef from #angular/core to dynamic.form.component.ts
Add private cdr: ChangeDetectorRef to the constructor
and add this.cdr.detectChanges(); to the end of your clear() method.
This will let know angular pick up that changes have happened and it won't think it made a mistake
P.S. the lines that are culprits are
<div class="errorMessage" *ngIf=" form && !form.controls[question.key].valid">{{question.label}} is required</div>
and
group[question.key] = question.required ? new FormControl(question.value || '', Validators.required) : new FormControl(question.value || ''); });

Categories

Resources