How to get a directive to react to an EventEmitter in a component - angular9

I have a CustomComponent which emits a value (let's just call it "error") if a http request to the back end api returns an error. How can I get a directive (call it Form Directive), applied to this form, to recognize when the "error" value is emitted by CustomComponent?
Code for CustomComponent:
export class CustomComponent extends FormComponent<Custom> {
constructor(
protected fb: FormBuilder,
private httpService: HttpService) {
super(fb);
}
currentVal: string = '';
inputType: string = 'password';
showPasswordTitle: string = 'Show Password';
showPasswordStatus: boolean = false;
form: FormGroup;
#Output() invalidOnError = new EventEmitter<string>();
protected buildForm(): FormGroup {
return this.form = this.fb.group({
fieldA: ['', Validators.required],
fieldB: ['', Validators.required],
fieldC: [''],
fieldD: ['', [Validators.required, Validators.pattern('[0-9]{10}')]]
}
protected doSubmit(): Observable<Custom> {
return this.httpService.callDatabase<Custom>('post', '/api/users/custom', this.value);
};
protected get value(): Registration {
return {
fieldA: this.fieldA.value,
fieldB: this.fieldB.value,
fieldC: this.fieldC.value,
fieldD: this.fieldD.value
};
}
get fieldA() { return this.form.get('fieldA'); }
get fieldB() { return this.form.get('fieldB'); }
get fieldC() { return this.form.get('fieldC'); }
get fieldD() { return this.form.get('fieldD'); }
protected onError() {
if (this.error.length) {//error.length indicates some of the fields in the form are already registered in the database
Object.keys(this.error).forEach(element => {
let formControl = this.form.get(this.error[element])
this.currentVal = formControl.value;
formControl.setValidators(formControl.validator ? [formControl.validator, unique(this.currentVal)] : unique(this.currentVal))
formControl.updateValueAndValidity()
this.invalidOnError.emit('error');
})
}
}
Code for FormComponent:
export abstract class FormComponent<T> implements OnInit {
protected form: FormGroup = null;
submitted = false;
completed = false;
error: string = null;
constructor(protected fb: FormBuilder) {}
ngOnInit() {
this.form = this.buildForm();
}
onSubmit() {
this.submitted = true;
if (this.form.valid) {
this.doSubmit().subscribe(
() => {
this.error = null;
this.onSuccess();
},
err => {
this.error = err
this.onError();
},
() => {
this.submitted = false;
this.completed = true;
}
)
}
}
protected abstract get value(): T;
protected abstract buildForm(): FormGroup;
protected abstract doSubmit(): Observable<T>;
protected onSuccess() {}
protected onError() {}
}
Code for Form Directive (works well when user clicks Submit button, which triggers onSubmit event in CustomComponent):
#Directive({
selector: 'form'
})
export class FormSubmitDirective {
submit$ = fromEvent(this.element, 'submit').pipe(shareReplay(1));
constructor(private host: ElementRef<HTMLFormElement>) {}
get element() {
return this.host.nativeElement;
}
}
I was hoping something like this could be the solution to my question, but this for sure doesn't work.
invalidOnError$ = fromEvent(this.element, 'error').pipe(shareReplay(1));
The idea is to use submit$ or invalidOnError$ from the directive to focus on the first invalid field in the form. Works fine for submit$, but not invalidOnError$. Appreciate some help - still fairly new to Angular.

I got this to work in a round about manner, by using the #Input decorator in another form directive which also imports submit$ from Form Directive.
No changes to code for FormComponent and Form Directive vs. what's shown in the question.
Relevant code from Custom component:
export class CustomComponent extends FormComponent<Custom> {
invalidOnError: string = '';
form: FormGroup;
protected buildForm(): FormGroup {
return this.form = this.fb.group({
fieldA: ['', Validators.required],
fieldB: ['', Validators.required],
fieldC: [''],
fieldD: ['', [Validators.required, Validators.pattern('[0-9]{10}')]]
}
protected doSubmit(): Observable<Custom> {
invalidOnError = '';
return this.httpService.callDatabase<Custom>('post', '/api/users/custom', this.value);
};
protected get value(): Registration {
return {
fieldA: this.fieldA.value,
fieldB: this.fieldB.value,
fieldC: this.fieldC.value,
fieldD: this.fieldD.value
};
}
get fieldA() { return this.form.get('fieldA'); }
get fieldB() { return this.form.get('fieldB'); }
get fieldC() { return this.form.get('fieldC'); }
get fieldD() { return this.form.get('fieldD'); }
protected onError() {
if (this.error.length) {//error.length indicates some of the fields in the form are already registered in the database
invalidOnError = 'invalid'
Object.keys(this.error).forEach(element => {
let formControl = this.form.get(this.error[element])
this.currentVal = formControl.value;
formControl.setValidators(formControl.validator ? [formControl.validator, unique(this.currentVal)] : unique(this.currentVal))
formControl.updateValueAndValidity()
this.invalidOnError.emit('error');
})
}
}
Relevant code from CustomComponentTemplate:
<form class="bg-light border" appFocus="FieldA" [formGroup]="CustomForm"
[invalidOnError]="invalidOnError" (ngSubmit)="onSubmit()">
Relevant code from invalidFormControlDirective (imports submit$ from Form Directive):
#Directive({
selector: 'form[formGroup]'
})
export class FormInvalidControlDirective {
private form: FormGroup;
private submit$: Observable<Event>;
#Input() invalidOnError: string = ''; //this is the #Input variable invalidOnError
constructor(
#Host() private formSubmit: FormDirective,
#Host() private formGroup: FormGroupDirective,
#Self() private el: ElementRef<HTMLFormElement>
) {
this.submit$ = this.formSubmit.submit$;
}
ngOnInit() {
this.form = this.formGroup.form;
this.submit$.pipe(untilDestroyed(this)).subscribe(() => {
if (this.form.invalid) {
const invalidName = this.findInvalidControlsRecursive(this.form)[0];
this.getFormElementByControlName(invalidName).focus();
}
});
}
ngOnChanges(){
of(this.invalidOnError).pipe(filter(val => val == 'invalid')).subscribe(() => {
if (this.form.invalid) {
const invalidName = this.findInvalidControlsRecursive(this.form)[0];
this.getFormElementByControlName(invalidName).focus();
}
});
}
ngOnDestroy() { }
// findInvalidControlsRecursive and getFormElementByControlName defined functions to get invalid controls references
}
That said, I'd be interested in 1) somehow bringing code under onChanges lifecyle into ngOnInit lifecyle in invalidFormControlDirective (couldn't get that to work), and 2) find out if there is some way to emitting an event and processing it with Rxjs fromEventPattern as opposed to passing the #Input variable invalidOnError into invalidFormControlDirective.

Related

Angular Trouble Emptying Array

Hell, I have an e-commerce application in which I am trying to empty the shopping cart after payment is successful, no matter what I have tried the array will not empty. I have tried cartItems.length = 0,
cartItems = [] as well as splice. I must be missing something. Code snippet below I will walk you through it.
This is my Model
import { Product } from './product';
export class CartItem {
id: number;
productId: number;
productName: string;
qty: number;
price: number;
size:string;
imageUrl:string;
constructor(id:number, size:string,
product:Product, qty= 1) {
this.id = id;
this.productId = product.id;
this.price = product.price;
this.size = size;
this.productName = product.name;
this.qty = qty;
this.imageUrl = product.imageUrl;
}
}
This is my car service as you can see I remove and add to cart as well as get cart items, there is no problem here. The remove and add are button clicks on html
export class CartService {
product: any;
cartItem: CartItem[] = [];
cartUrl = 'http://localhost:4000/cart';
constructor(private http: HttpClient, private router: Router,
private route: ActivatedRoute) {
}
getCartItems(): Observable<CartItem[]> {
return this.http.get<CartItem[]>(cartUrl).pipe(
map((result: any[]) => {
let cartItems: CartItem[] =[];
for(let item of result) {
cartItems.push( new CartItem(item.id, item.size,
item.product, item.imageUrl ));
}
return cartItems;
})
);
}
addProductToCart(product:Product):Observable<any>{
return this.http.post(cartUrl, {product});
}
RemoveProductFromCart(id:number):Observable<void>{
//this.cartItems.splice(index,1)
alert("show deleted item");
return this.http.delete<CartItem[]>(`${this.cartUrl}/${id}`)
.pipe(catchError(_err => of (null))
);
}
buttonClick() {
const currentUrl = this.router.url;
this.router.navigateByUrl('/', {skipLocationChange:
true}).then(() => {
this.router.navigate([currentUrl]);
});
//alert("Should Reload");
}
addPurseToCart(product:Product):Observable<any>{
return this.http.post(cartUrl, {product})
}
}
Here is the checkout component, I injected the cart component so I could call the the empty cart function which resides in the cart component. I did import the CartComponent. Check out is based on cart. When the cart is emptied so should checkout
#Component({
providers:[CartComponent], //injected CartComponent
selector: 'app-checkout',
templateUrl: './checkout.component.html',
styleUrls: ['./checkout.component.scss']
})
export class CheckoutComponent implements OnInit {
// #ViewChild(CartComponent) // #ViewChild(CartItemComponent) cartitemComponent: CartItemComponent
cartComponent: CartComponent
#Input() product: CartItem;
#Input() cartItem: CartItem;
cartUrl = 'http://localhost:4000/cart';
size;
cartItems = [];
cartTotal = 0;
itemTotal = 0;
shipping = 8.00;
estimatedTax = 0;
myValue: any;
constructor(private msg: MessengerService, private route:
ActivatedRoute,
private router: Router, private
cartService:CartService,
private productService: ProductService, private
comp:CartComponent) {}
ngOnInit() {
this.loadCartItems();
}
}
loadCartItems(){
this.cartService.getCartItems().subscribe((items:
CartItem[]) => {
this.cartItems = items;
this.calcCartTotal();
this.calNumberOfItems();
})
}
calcCartTotal() {
this.cartTotal = 0;
this.cartItems.forEach(item => {
this.cartTotal += (item.qty * item.price);
})
this.cartTotal += this.shipping;
this.myValue = this.cartTotal
render(
{
id:"#paypal-button-container",
currency: "USD",
value: this.myValue,
onApprove: (details) =>{
alert("Transaction Suceessfull")
console.log(this.myValue);
this.comp.handleEmptyCart();
}
}
);
}
calNumberOfItems(){
console.log("Trying to get tolal items")
this.itemTotal = 0;
this.cartItems.forEach(item => {
this.itemTotal += item.qty;
})
}
}
cart component
export class CartComponent implements OnInit {
#Input() product: CartItem;
#Input() cartItem: CartItem;
//items: CartItem [] =[];
cartUrl = 'http://localhost:4000/cart';
val;
size;
cartItems = [];
cartTotal = 0
itemTotal = 0
constructor(private msg: MessengerService, private
cartService:CartService, private productService:
ProductService, private formBuilder:FormBuilder, private
_data:AppserviceService, private router:Router) { }
ngOnInit(): void {
this.handleSubscription();
this.loadCartItems();
}
handleSubscription(){
this.msg.getMsg().subscribe((product: Product) => {
})
}
loadCartItems(){
this.cartService.getCartItems().subscribe((items:
CartItem[]) => {
this.cartItems = items;
console.log("what is in cartItems" + this.cartItems)
console.log("What does this property hold" +
this.cartItem)
this.calcCartTotal();
this.calNumberOfItems();
})
}
calcCartTotal() {
this.cartTotal = 0
this.cartItems.forEach(item => {
this.cartTotal += (item.qty * item.price)
})
}
calNumberOfItems(){
console.log("Trying to get tolal items")
this.itemTotal = 0
this.cartItems.forEach(item => {
this.itemTotal += item.qty
})
}
handleEmptyCart(){
alert("Hit Empty Cart");
/*here I get the cart items to see what is in the array
and try to empty, it does show tow objects in the
array*/
this.cartService.getCartItems().subscribe((items:
CartItem[]) => {
this.cartItems = items;
this.cartItems.length=0
// this.cartItems = [];
console.log("what is in cartItems" + this.cartItems)
})
}
}
I have used different approaches trying to empty the cart nothing works. It makes me think I'm stepping on something or somehow creating events calling the loadCartItems to many times not sure but according to my research one of these approaches should work. If someone can please help me out I'm stuck. I would greatly appreciate it.
Short answer: run change detection ChangeDetectorRef.detectChanges()
Long answer:
You need to understand how angular works.
Lets assume simple example:
<div id="val">{{val}}</div><button (click)="val++">Increase<div>
So when u click button variable changes, but who changes actual #val div content? Answer is that Angular uses zone.js to patch/change a lot of functions to run Angular change detection after most of JS events (you can control type of this events to include/exclude some of them). Also Promise is patched same way, thats why after some Promise is resolve change detection is run.
However, here you run some render method with onApprove callback which is probably some 3rd party library (?) and zone.js is not aware of it (https://angular.io/guide/zone#ngzone-run-and-runoutsideofangular). And though running detectChanges gonna help u, u better re-write your code, so onApprove is always in Angular zone and u never gonna face similar bugs in future when using this method.

I cant add the Input Decorator in Angular

I'm getting data from father component with Input in Angular, but it doen't work
My child:
export class ForumComponent implements OnInit {
#Input() id_foro: number = 3;
nombre: string = '';
desc: string = ''
forum = this.forumService.getById(this.id_foro).subscribe((data: Forum[]) => {
this.nombre = data[0].name
this.desc = data[0].description
});
constructor(private forumService: ForumService) { }
where I call it:
<div *ngIf="forum.id_category_fk === category.id">
<app-forum [id_foro]="forum.id"></app-forum>
</div>
That is not the correct way to call the method and assign the value.
For your scenario, you should call the service method in ngOnInit().
export class ForumComponent implements OnInit {
#Input() id_foro: number = 3;
...
forum: Forum;
...
ngOnInit() {
this.forumService.getById(this.id_foro).subscribe((data: Forum[]) => {
this.nombre = data[0].name;
this.desc = data[0].description;
this.forum = data[0];
});
}
}

Angular 8: Send Form data as a Service to Another Component

How do I sent all the Formgroup data as a Service in Angular to Another Component? I am looking for alternative to ControlValueAccessor. So anytime someone writes something on a form, the receiver will get the value data.
Trying to edit the code below to work.
Address Sender Form:
export class AddressFormComponent implements OnInit {
editAddressForm: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.editAddressForm = this.formBuilder.group({
'streetName' : [null, Validators.required, Validators.maxLength(64)],
'city' : [null, Validators.required, Validators.maxLength(32)],
'state' : [null, Validators.required, Validators.maxLength(16)],
'postalCode' : [null, Validators.required, Validators.maxLength(16)]
});
}
ngOnInit() {
}
// What should I write as Event Code??
}
Service:
export class AddressService {
private messageSource = new Subject();
currentMessage = this.messageSource.asObservable();
constructor() { }
changeMessage(currentMessage) {
this.messageSource.next(currentMessage);
}
}
Receiver:
export class AddressCombinedReceiverComponent implements OnInit {
message: any;
constructor(private readonly addressService: AddressService) {
this.addressService.currentMessage.subscribe(currentMessage => this.message = currentMessage);
}
ngOnInit() {
}
}
Related question: Angular 2 - formControlName inside component
This doesnt look far away from what you need, in your AddressFormComponent try the following. Ensure to clean up the subscription when needed, added below.
public subs = new Subscription();
public editAddressForm: FormGroup;
constructor(addressService: AddressService){}
public ngOnInit(): void {
this.editAddressForm = this.formBuilder.group({
'streetName' : [null, Validators.required, Validators.maxLength(64)],
'city' : [null, Validators.required, Validators.maxLength(32)],
'state' : [null, Validators.required, Validators.maxLength(16)],
'postalCode' : [null, Validators.required, Validators.maxLength(16)]
});
this.subs.add(this.editAddressForm.valueChanges.subscribe(data =>
{
this.addressService.changeMessage(this.editAddressForm);
}));
}
public ngOnDestroy(): void
{
this.subs.unsubscribe();
}
That should in turn push the data to the subject you have an in turn cause the subscribers of that subject to receive the data. To confirm try the following
export class AddressCombinedReceiverComponent implements OnInit {
public message: any;
constructor(private readonly addressService: AddressService) {}
public ngOnInit() {
this.addressService.currentMessage.subscribe(currentMessage => {
this.message = currentMessage;
console.log(this.message); // should be the form coming through
});
}
}
You don't need to respond to an event. FormGroups already have an observable that streams the changes.
Example
// address-form.component.ts
export class AppComponent implements OnDestroy {
editAddressForm: FormGroup;
ngOnDestroy() {
// cleanup
this.addressService.formValues$ = null;
}
constructor(private addressService: AddressService) {
this.editAddressForm = new FormGroup({
streetName: new FormControl(null, { validators: [Validators.required, Validators.maxLength(64)]}),
city: new FormControl(null, { validators: [Validators.required, Validators.maxLength(32)]}),
state: new FormControl(null, { validators: [Validators.required, Validators.maxLength(16)]}),
postalCode: new FormControl(null, { validators: [Validators.required, Validators.maxLength(16)]}),
});
this.addressService.formValues$ = this.editAddressForm.valueChanges;
}
}
// address.service.ts
export class AddressService {
formValues$: Observable<any>;
constructor() { }
}
In regards to the receiver, if the intention is to show the form changes in the template, then I recommend using the async pipe for that, so that you only subscribe when it's defined, like:
// address-combined-receiver.component.ts
export class SimpleComponent implements OnInit {
addressValues$: Observable<any>;
constructor(private addressService: AddressService) {
this.addressValues$ = this.addressService.formValues$;
}
ngOnInit() {
}
}
<!-- address-combined-receiver.component.html -->
<ng-container *ngIf="addressValues$ | async as addressValues">
<pre>{{ addressValues | json }}</pre>
</ng-container>

Property 'filter' does not exist on type 'Object'. When trying to filter response

Im trying to get data from a json file that equal the name of the player in the url. For example: localhost:4200/players/Febiven should only return the information about Febiven. Im using Angular 6
So far I have this code:
player.service.ts
get(ingameName){
return <Observable<Player>> this.http.get(endpoint).map(response =>{
let data = response.filter(item=>{
if (item.ingameName == ingameName) {
return item
}
});
if (data.length == 1){
return data[0]
}
return {}
})
.catch(this.handleError)
}
private handleError(error:any, caught:any): any{
console.log(error, caught)
}
player-info.component.ts
export interface Player {
ingameName: string;
name: string;
intro: string;
image: string;
info: string;
team: string;
dob: string;
earnings: string;
role: string;
game: string;
favourite: string;
IDs: string;
}
export class PlayerInfoComponent implements OnInit {
players: Player[] = null;
private routeSub:any;
private req:any;
ingameName:string;
player : player;
constructor(private route: ActivatedRoute, private plService : PlayerService) { }
ngOnInit() {
this.routeSub = this.route.params.subscribe(params => {
this.ingameName = params['ingameName'];
this.req = this.plService.get(this.ingameName).subscribe(data=>{
this.player = data as player
})
});
Im getting the error 'Property 'filter' does not exist on type 'Object'. And I don't really have an idea how to fix this, I looked at multiple answers, but none seemed to work for me. If someone could help me with fixing this error thatd be great
Thanks
filter only exists on arrays. Your response is an object. You can do this instead:
get(ingameName){
return <Observable<Player>> this.http.get(endpoint).map(response =>{
let data = response.json();
if (data.ingameName == ingameName){
return data;
}
return {};
})
.catch(this.handleError)
}
Try this it will work:
define a parameter inside your class & use it in ngOnInit() function like this:
export class VideoDetailComponent implements OnInit, OnDestroy {
data_new:any;
ngOnInit() {
this.http.get("assets/json/videos.json").subscribe(data =>{
this.data_new = data;
this.data_new.filter(item=>{
console.log(item)
// do your work here
})
})
}
}

Angular 4 : How to use pattern in an input list

I have a form html and the button submit becomes enable only when the form is valid. That is, when a particular input contains the recommended pattern. I need to use this pattern in an inputlist. It works with a simple input but with the input list, the list disappears :
<input list="refNumbers" pattern="[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]" formControlName="refNb" type="text" name="article" maxlength="8" size="15" required title="8 characters" />
<datalist id="refNumbers">
<option *ngFor="let ref of listOfArticles">{{ref.refNumber.input}}</option>
</datalist>
Otherwise, how can i disable button submit if the ref number selected is not in the datalist ? Because it seems that the valid condition on the form is not enough :
<button type="submit" [disabled]="!myFormGroup.valid" >Valider</button>
component.ts :
import { Component, OnInit } from '#angular/core';
import 'rxjs/add/operator/switchMap';
import { ManagementArbologistiqueService } from "../management-arbologistique.service";
import { ActivatedRoute, Params } from '#angular/router';
import { FormGroup, FormControl, FormBuilder, FormArray, Validators } from '#angular/forms';
#Component({
selector: 'app-arbologistique',
templateUrl: './arbologistique.component.html',
styleUrls: ['./arbologistique.component.css']
})
export class ArbologistiqueComponent implements OnInit {
private reponseTest: String;
private listOfArticles :Array<Object>
private pathDownload: any;
private myFormGroup: FormGroup;
fileToUpload: File = null;
private buttonSubmitEnabled: boolean = false;
constructor(public fb: FormBuilder, private managementArbo: ManagementArbologistiqueService, private route: ActivatedRoute) { }
ngOnInit() {
this.myFormGroup = this.fb.group({
itemRows: this.fb.array([this.initItemRows()])
})
this.myFormGroup.valueChanges.subscribe(x => this.buttonSubmitEnabled = false);
this.getListBdd();
}
initItemRows() {
return this.fb.group({
... //other fields
refNb: ['',Validators.required],
... //other fields
})
}
addRow(index: number) {
console.log("functionAddRow called");
const control = <FormArray>this.myFormGroup.controls['itemRows'];
control.insert(index, this.initItemRows());
}
deleteRow(index: number) {
console.log("functionDeleteRow called");
const control = <FormArray>this.myFormGroup.controls['itemRows'];
control.removeAt(index);
}
sendForm() {
this.buttonSubmitEnabled=true;
console.log("functionExportCalled");
this.route.params.subscribe((params: Params) => {
let subroute = "exportation";
this.managementArbo.postProducts(subroute, JSON.stringify(this.myFormGroup.value))
.subscribe(
res => { this.reponseTest = res; console.log('reponse:' + res); }
,
err => console.log(err),
() => console.log('getProducts done'));
});
}
getListBdd() {
this.route.params.subscribe((params: Params) => {
let subroute = "getRefNumber";
this.managementArbo.getProducts(subroute)
.subscribe(
res => { this.listOfArticles = res; console.log('reponse:' + res); }
,
err => console.log(err),
() => console.log('getProducts done'));
});
}
get refNb() {
return this.myFormGroup.get('itemRows.refNb');
}
}
If you want to add Validation in reactive form you can use build in Validators
initItemRows() {
const regEx="[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]";
return this.fb.group({
... //other fields
refNb: ['',Validators.required,Validators.pattern(regEx)],
... //other fields
})
}
<button type="submit" [disabled]="!myFormGroup.valid" >Valider</button>