Is it just a good rule of thumb to handle all 'disable' validation logic on the form component itself? Does that sound right??
I'm really just trying to find a way to share the 'disable' logic across the application without repeating it in every form component, but i guess that is the proper way to do things?? Can someone verify this??
I would like to create a reusable submit button component.
This submit button component should act like any other submit button component, except for one thing...
The submit button needs to "disable itself" after being clicked.
That should be easy enough right. However, the rub here is that the button also needs to "re-enable itself" after the "call" is 100% completed. (In case there is an error, or the application needs to allow another action after the first is completed, etc).
I would like 100% of "that" logic to exist inside of the component so I can easily reuse it everywhere in the application.
I thought it would be kinda easy, but I think I'm still missing something...
I guess the idea would be to use an #Input() preferably (or maybe Output) in order to pass in some "async callback type of thing" to the button control itself...
That way the button could react to the "async callback type of thing" completing and use the callback handler to re-enable itself.
Thanks for the help in advance!
I wrote an abstract class which I use all the time for this exact case:
import { ElementRef, EventEmitter, Input, Output, ViewChild } from '#angular/core';
import { FormGroup, FormGroupDirective } from '#angular/forms';
export abstract class FormBase<T = any> {
#Input() isSubmitting: boolean;
#Output('onSubmit') _submit = new EventEmitter<T>();
#ViewChild(FormGroupDirective, { static: true })
ngForm: FormGroupDirective;
#ViewChild('submitButton', { static: true }) button: ElementRef;
form: FormGroup;
onSubmit(): void {
if (this.isFormValid()) this._submit.emit(this.getFormValue());
}
submit(): void {
if (!this.button || !this.button.nativeElement) return;
this.button.nativeElement.click();
}
reset(value?: any) {
this.ngForm.resetForm(value);
}
isFormValid(): boolean {
return this.form.valid;
}
getFormValue(): T {
return this.form.value;
}
shouldDisable(): boolean {
return (
this.isSubmitting ||
((this.form.invalid || this.form.pending) &&
(this.ngForm ? this.ngForm.submitted : true))
);
}
}
component.ts
import { FormBase } from 'path';
#Component({
...
})
export class FormComponent extends FormBase {
constructor(formBuilder: FormBuilder) {
super();
this.form = formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required],
});
}
}
component.html
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<mat-form-field>
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="username" />
</mat-form-field>
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="text" formControlName="password" />
</mat-form-field>
<button [disabled]="shouldDisable()" mat-flat-button color="primary">
Submit
</button>
</form>
Your form components basically extend from this class which should work 99% of the time. If your component needs some very specific functionality like changing when the button becomes disabled or something else, you can simply override the methods in the FormComponent.
Related
iam trying to use RouteGuard in my app to prevent unauthorized users from accessing it. My can activate method in Guard looks like this:
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.statusService.appStatus.pipe(filter(status=>{
return status.selectedAgency!=null;
}),map(status=>{
console.log(status)
if(status.selectedAgency.allowPreEdit){
return true;
}
else{
return false;
}
}))
}
Iam trying to filter default value from appStatus observable, because its null for selectedAgency. Later the agency is initialized from home component via changeSelectedAgency method. The problem is that it doesnt work - if i use filter method, guard just receives nothing - even the console.log in map method doesnt occur. Any ideas what am i doing wrong? Why it doesnt wait for result? Init Agency just doesnt occur, iam left on a blank screen with no error in console. There must be some problem with the guard - it doesnt let the agency init, but i dont know why. Any ideas.
StatusService looks like this:
export class StatusService {
private statusSubj= new BehaviorSubject<AppStatus>({selectedMonth:new Date(new Date().getFullYear(),new Date().getMonth()),selectedAgency:null});
get appStatus(){
return this.statusSubj.asObservable();
}
changeSelectedAgency(selectedAgency:Agency){
console.log("changeagency");
this.appStatus.pipe(take(1)).subscribe(currentStatus=>{
this.statusSubj.next({...currentStatus,selectedAgency:selectedAgency})
})
}
...
}
export class HomeComponent{
user$:Observable<User>;
agencies:Agency[]=[];
selectedAgency:Agency=null;
commonLoaded=false;
constructor(private breakpointObserver: BreakpointObserver, private userService:UserService, private planService:PlanService, private statusService:StatusService, private commonService:CommonService) {
this.user$=this.userService.currentUser;
forkJoin([this.planService.getAgencies(new Date()),this.commonService.loadTypes()]).subscribe(data=>{
this.initAgencies(data[0]);
console.log(data);
this.commonLoaded=true;
})
}
private initAgencies(agList:Agency[]){
this.agencies=agList;
console.log("init");
this.statusService.changeSelectedAgency(agList[0]);
this.selectedAgency=agList[0];
}
home component html:
<ng-container *ngIf="commonLoaded">
...
<div class="controls-toolbar" *ngIf="agencies.length>0" fxLayoutGap="30px" fxLayoutAlign="left center">
<app-monthpicker></app-monthpicker>
<mat-form-field appearance="fill" class="no-padding no-underline">
<mat-label>Vybraná činnost</mat-label>
<mat-select [(ngModel)]="selectedAgency" (ngModelChange)="changeAgency($event)" [compareWith]="compareAgencies">
<mat-option [value]="agency" *ngFor="let agency of agencies">Činnost č. {{agency.agNum}}</mat-option>
</mat-select>
</mat-form-field>
<div class="workplace-info" fxLayout fxLayoutAlign="center center" fxLayoutGap="10px"><mat-icon>home</mat-icon><p>{{selectedAgency.wpDesc}}</p></div>
</div>
<div class="content-container" *ngIf="selectedAgency">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</ng-container>
And finally routes:
const routes: Routes = [
{path:"",component:HomeComponent, canActivate:[AuthGuard],
children:[
{path:'',redirectTo:"timesheet",pathMatch:"full"},
{path:'timesheet',component:TimesheetComponent},
...
{path:"shiftRecords",component:ShiftRecordsComponent, canActivate:[PreEditGuard]},
{path:'passchange',component:PassChangeComponent}
]},
{path:'login',component:LoginComponent},
{path:"**",redirectTo:''}
selectedAgency:null in the BehaviorSubject is the problem here.
Because of this, filter gives you an empty observable, so map doesn't run. This leads to canActivate returning observable with falsy value, so routing is prevented and HomeComponent is not initialised, so, the constructor doesn't run and initAgencies is not called.
So, it is kind of a deadlock situation, canActivate depends on component initialisation (to call initAgencies and set selectedAgency ) but component initialisation depends on canActivate (to return true, so that routing and initialisation can take place).
So, you can provide some proper default value to selectedAgency or tweak your logic in some other way.
I have a problem with window event beforeunload in Angular app.
i wants to check if the user navigate to another page, if so i want to clean sessionStorage. I use.
onBeforeUnload(event: BeforeUnloadEvent) {
event.returnValue = '';
}
Beforeunload event also workd as a page refresh. How to check if the user leaves the application and clear the sesion storage after confirm dialog.
You can try below code in constructor of your component class.
Option 1
window.addEventListener("beforeunload", function (e) {
exampleService.logout();
});
Option 2
#HostListener('window:beforeunload', ['$event'])
beforeUnloadHandler(event: any) {
event.preventDefault();
// any other code / dialog logic
}
To distinguish from 2 cases, you should check out this stackoverflow question or related questions, here's one:
Question
Hope, this helps.
The following code will make browser to ask user that whether he wants to lose the unsaved changes or not.
Also, I'm checking whether the form is dirty or not.
If the form is not dirty, browser won't bother the user and close it.
import { Component, ViewChild, HostListener } from '#angular/core';
import { NgForm } from '#angular/forms';
import { DetailedUser } from 'src/app/models/detailed-user';
#Component({
selector: 'app-member-edit',
templateUrl: './member-edit.component.html',
styleUrls: ['./member-edit.component.css']
})
export class MemberEditComponent {
user: DetailedUser = new DetailedUser();
#ViewChild('userForm') userForm: NgForm;
#HostListener('window:beforeunload', ['$event'])
WindowBeforeUnoad($event: any) {
if (this.userForm.dirty) {
$event.returnValue = 'Your data will be lost!';
}
}
saveChanges(){
....
this.userForm.reset(this.user);
}
}
the following is the html section
<form (ngSubmit)="saveChanges()" #userForm="ngForm">
<div class="form-group">
<label for="introduction">Introduction</label>
<textarea
name="introduction"
class="form-control"
[(ngModel)]="user.introduction"
rows="6"></textarea>
</div>
<div class="form-group">
<label for="lookingFor">Looking For</label>
<textarea
name="lookingFor"
class="form-control"
[(ngModel)]="user.lookingFor"
rows="6"></textarea>
</div>
</form>
I've developed a component with two views. Component A has a contact form, and Component B is the "Thank you" page.
Component A:
You fill the form and submit it.
As soon as the response arrives, a new ReplaySubject value is created.
The user will be routed to the component B.
Component B:
The component is initialized.
The component gets the value from the subject.
The view is rendered and displays the thank you message.
HTTP Call response (Returned after a successful Form data post request):
{
"status": 200,
"body": {
"message": "We have received your request with the card information below and it will take 48h to be processed. Thank you!",
"card": {
"name": "John Doe",
"email": "john.doe#gmail.com",
"accountNumber": "12345-67890"
}
},
"type": "ItemCreated"
}
Component A (Form) code:
import { Component } from '#angular/core';
import { FormBuilder, Validators } from '#angular/forms';
import { RequestCardWidgetService } from './request-card-widget.service';
import { RouterService } from '#framework/foundation/core';
import { Item } from '#pt/request-card-data'
#Component({
selector: 'pt-request-card-form',
templateUrl: './request-card-form.template.html',
providers: [RouterService]
})
export class RequestCardFormComponent {
constructor(private fb: FormBuilder, private data: RequestCardWidgetService, private router: RouterService){}
item: Item = {
name: '',
email: '',
accountNumber: ''
};
requestCardForm = this.fb.group({
name: ['', Validators.required],
email: ['', Validators.email],
accountNumber: ['', Validators.required]
})
onSubmit() {
this.item = this.requestCardForm.value;
this.data.requestCard(this.item)
.subscribe(data => {
this.data.processResult(data);
this.router.navigate(['/success']);
});
}
}
Component B (Thank you page) code:
import { Component } from '#angular/core';
import { RequestCardWidgetService } from './request-card-widget.service';
#Component({
selector: 'pt-request-card-success',
templateUrl: './request-card-success.template.html'
})
export class RequestCardSuccessComponent {
messages: any; // TODO: To use the proper type...
constructor( private requestCardService: RequestCardWidgetService) {
this.messages = this.requestCardService.message;
}
}
Component B Template (Thank you page):
<div *ngIf='(messages | async) as msg'>
{{ msg.message}}
</div>
Component Service code:
import { Injectable } from '#angular/core';
import { HttpResponse } from '#angular/common/http';
import { Observable, ReplaySubject } from 'rxjs';
import { map, take } from 'rxjs/operators';
import {
RequestCardDataService,
Item,
ItemCreated
} from '#example/request-card-data';
#Injectable()
export class RequestCardWidgetService {
constructor(private dataService: RequestCardDataService) { }
private readonly results = new ReplaySubject<ItemCreated>();
readonly message: Observable<ItemCreated> = this.results; // Message Line. This is the variable that I'm rendering in the template. Is this the correct way of extracting subject values?
requestCard (card: Item): Observable<ItemCreated> {
return this.dataService.postCardRecord(card).pipe(
take(1),
map((response: HttpResponse<ItemCreated>): ItemCreated | {} => {
return response.body
? response.body
: {};
})
);
}
processResult(data: ItemCreated) {
this.results.next(data);
}
}
Recap:
Component A has a form. After you submit the form, the results are stored as a new value in the subject. The user is routed to the thank you page.
The thank you page component renders the element and gets the newest value from the subject. Then it renders the contents.
This code works, but I do have some questions.
Question:
Is this the proper way of using the Subject?
Is this:
readonly message: Observable<ItemCreated> = this.results;
the proper way of extracting the values from a subject? (I'm passing 'message' to the view.)
Are there better ways to achieve the same result?
Thanks a lot in advance.
Is this the right way to use a subject?
ReplaySubect unconstrained will replay all of the values it has previously emitted to new subscribers. This could lead to situations where a user could receive previously emitted messages until they finally receive the current message. Therefore, either constrain the subject or consider using a BehaviorSubject instead.
Extracting values from the subject
A Subject and all of its derivatives are both Observables and Observers. When providing a subject to a consumer, you do not want to expose the Observer interface, i.e., a consumer should never be able to call next, error or complete. Thus, as suggested in a comment, you should ensure you are only exposing the Observable interface to consumers by first calling the asObservable method.
readonly message: Observable<ItemCreated> = this.results.asObservable();
Next Steps
If you want to continue using service-based communication between components, then I think you have opportunities to clean/refine your code per the docs linked in the question comments.
If your application is going to grow in complexity, I would steer you down the Redux-style architecture and look into NgRx and specifically, the use of effects to manage side effects.
Effects can meet all of your requirements with simple, discreet observable constructs, i.e., an effect to handle the form submission, receive the response, even navigate to the success page. More information about effects can be found here.
A redux architecture can be overkill for simple tasks, but if you're working on a large app managing a large state tree, I prefer this approach over service-based integrations.
I currently have this code in my app.component.ts
app.component.html
<div [ngClass]="myclass">
...rest of the content here
</div>
This I have the this:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
app.component.ts
export class AppComponent {
myclass: string;
changeClass(myclass) {
this.myclass = myclass;
}
}
Now, all this works fine BUT I now want to put the triggering button on another component.
If I put this on another component:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
How can I get it to change the class?
There are two ways you can do this you can use output with an EventEmit
Or you can set up a service that monitors the changes to a variable and use that as the control point for the change.
Personally, I use services for this instance as its easier to manage the code and its flow.
This answer has all the code in you need to look at.
Changing a value in two different components at the same time Angular 2
Hope that helps
There are at least two options. Subject and Observable or if this another component is a parent you can use #Input.
Subject and Observable method:
angular guide Highly recommended to read whole page.
Some component
export class SomeComponent {
constructor(private ClassService: ClassService) { }
private changeClass(class) {
this.ClassService.changeClass(class);
}
}
Another Component
export class AnotherComponent implements OnInit, OnDestroy {
constructor(private ClassService: ClassService) { }
private class: string = "";
private subscribtion: Subscribtion;
ngOnInit(): void {
this.Subscribtion = this.ClassService.someClass$.subscribe(
(class) => { this.class = class; }
)
}
ngOnDestroy(): void {
this.Subscribtion.unsubscribe();
}
}
Service
#Injectable();
export class ClassService{
constructor() { }
private someClassSource= new Subject<string>();
someClass$= this.someClassSource.asObservable();
changeClass(class) {
this.someClassSource.next(class);
}
}
taken from my answer
#Input method:
angular guide
This is very simple, when you click button changeClass method will change elClass which will be passed to another component by #Input decorator, every change of #Input will cause a detect changes which will detect that value has changed so class will change to myClass.
Parent component
parent.component.html
<another-component [elementClass]="elClass"></another-component>
<button (click)="changeClass('myClass')">change class<button>
parent.component.ts
export class ParentComponnet {
private elClass: string = "";
changeClass(class: string) {
elClass = class;
}
}
Another component (must be child component)
another.component.html
<div [ngClass]="elementClass">
another.component.ts
export class AnotherComponent {
#Input() elementClass: string;
}
There is also Child to Parent interaction via #Output (emitting event) angular guide
A bit tricky situation. For the code below, I have added (keydown.enter)="false" to ignore the break line/enter button in textarea
This is causing a user issue and would like the existing behaviour where Pressing enter should automatically trigger the "Save button"
Any idea how to trigger the Save button when still focusing in textArea but ignore the breakline?
<textarea #textArea
style="overflow:hidden; height:auto; resize:none;"
rows="1"
class="form-control"
[attr.placeholder]="placeholder"
[attr.maxlength]="maxlength"
[attr.autofocus]="autofocus"
[name]="name"
[attr.readonly]="readonly ? true : null"
[attr.required]="required ? true : null"
(input)="onUpdated($event)"
[tabindex]="skipTab ? -1 : ''"
(keydown.enter)="false"
[(ngModel)]="value">
</textarea >
Extending the answer by #Pengyy
You can bind the bind the enter key to a pseudoSave function, and preventDefault inside of that, thus preventing both the Save function and the newline. Then you can either call the save function from there(assuming it is accessible such as a service) or you can emit an EventEmitter, and have that emit get caught to trigger the Save function.
you can bind the same function of Save button to keydown.enter of texterea, and call $event.preventDefault to avoid the newline.
sample plunker.
Assuming that your textarea is inside a form element.
{Plunker Demo}
You can achieve it by using a hidden submit input, like this
#Component({
selector: 'my-app',
template: `
<form (submit)="formSubmitted($event)">
<input #proxySubmitBtn type="submit" [hidden]="true"/>
<textarea #textArea (keydown.enter)="$event.preventDefault(); proxySubmitBtn.click()">
</textarea>
</form>
`,
})
export class App {
formSubmitted(e) {
e.preventDefault();
alert('Form is submitted!');
}
}
You can create a service which can send a notification to other components that will handle the command. The service could look like this:
import { Injectable } from "#angular/core";
import { Subject } from "rxjs/Subject";
#Injectable()
export class DataSavingService {
private dataSavingRequested = new Subject<void>();
public dataSavingRequested$ = this.dataSavingRequested.asObservable();
public requestDataSaving(): void {
this.dataSavingRequested.next();
}
}
... and should be registered in the providers section of the module. Note: if data must be passed in the notification, you can declare a non-void parameter type for the dataSavingRequested Subject (e.g. string).
The service would be injected in the component with the textarea element and called in the handler of the Enter keypress event:
import { DataSavingService } from "./services/data-saving.service";
...
#Component({
template: `
<textarea (keypress.enter)="handleEnterKeyPress($event)" ...></textarea>
`
})
export class ComponentWithTextarea {
constructor(private dataSavingService: DataSavingService, ...) {
...
}
public handleEnterKeyPress(event: KeyboardEvent): void {
event.preventDefault(); // Prevent the insertion of a new line
this.dataSavingService.requestDataSaving();
}
...
}
The component with the Save button would subscribe to the dataSavingRequested$ notification of the service and save the data when notified:
import { Component, OnDestroy, ... } from "#angular/core";
import { Subscription } from "rxjs/Subscription";
import { DataSavingService } from "../services/data-saving.service";
...
#Component({
...
})
export class ComponentWithSaveButton implements OnDestroy {
private subscription: Subscription;
constructor(private dataSavingService: DataSavingService, ...) {
this.subscription = this.dataSavingService.dataSavingRequested$.subscribe(() => {
this.saveData();
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private saveData(): void {
// Perform data saving here
// Note: this method should also be called by the Save button
...
}
}
The code above assumes that the saving must be performed in the component with the Save button. An alternative would be to move that logic into the service, which would expose a saveData method that could be called by the components. The service would need to gather the data to save, however. It could be obtained with a Subject/Observable mechanism, or supplied directly by the components as a parameter to saveData or by calling another method of the service.
it could be 2 solutions:
Use javascript to handle enter event and trigger Save function in it
or
Use Same thing from Angular side as describe in this.
This may also help you