I am going to write some unit tests for my validator directive, but don't know how, even after googling and reading the Angular website. I will explain the codes (just the parts that are needed) here.
Here is my component.ts code:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup } from '#angular/forms';
import { isValidValidator } from '../../directives/is-valid.directive';
#Component({
selector: 'app-microplate',
templateUrl: './microplate.component.html',
styleUrls: ['./microplate.component.css']
})
export class MicroplateComponent implements OnInit {
form: FormGroup;
ngOnInit(): void {
this.form = new FormGroup({
columns: new FormControl('', [
isValidValidator()
])
});
}
}
Here is my directive.ts code:
import { Directive, Input } from '#angular/core';
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '#angular/forms';
const { isNumbersFieldValid } = require('../utils/utils.js');
export function isValidValidator(): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
return isNumbersFieldValid(control.value) ? {isValid: {value: control.value}} : null;
};
}
#Directive({
selector: '[appIsValid]',
providers: [{provide: NG_VALIDATORS, useExisting: IsValidDirective, multi: true}]
})
export class IsValidDirective implements Validator {
#Input('appIsValid') isValid: string;
validate(control: AbstractControl): {[key: string]: any} | null {
return this.isValid ? isValidValidator()(control) : null;
}
}
Here is my directive.spec.ts code:
import { IsValidDirective } from './is-valid.directive';
describe('IsValidDirective', () => {
it('should create an instance', () => {
const directive = new IsValidDirective();
expect(directive).toBeTruthy();
});
// How can I write some unit tests here to check if my directive works well for different input strings?
});
here you can find how to test attribute directives
https://angular.io/guide/testing-attribute-directives
basically you need to create a testComponent in your spec files and apply your directive in that testComponent, your test expectation can be done on the testComponent directly
Related
Whatever i do angular does not detect change on talks array. I have a handleSubmit function to send the toolbar. Toolbar use it to send the changes to parent from input field.
My app.component.ts file
import { Component, Type, OnChanges, SimpleChanges } from '#angular/core';
import { getResponse } from '../api/API';
declare module '../api/API' {
export interface NlpAPI {
getResponse(data: any): Promise<any>;
}
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges {
talks: string[];
title: string;
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
constructor() {
this.talks = [];
this.title = 'Talks';
}
ngOnInit() {
this.talks.push('Welcome to ProjectX! How can I help you?');
this.talks.push('I am a chatbot. I can help you with your queries.');
}
handleSubmit(data: any): void {
this.talks.push(data.talk);
}
messageResponse() {
// #ts-ignore: Object is possibly 'null'.
const x = document.getElementById('txt').value;
// #ts-ignore: Object is possibly 'null'.
document.getElementById('output').innerHTML =
'Your message is ' + '"' + x + '"';
}
}
My app.component.html
<!-- Toolbar -->
<app-custom-toolbar [handleSubmit]="handleSubmit"></app-custom-toolbar>
<!-- Highlight Card -->
<app-text-area [talksFromUser]="talks" [title]="title"></app-text-area>
<!-- Bottombar -->
<router-outlet></router-outlet>
My text-area.component.ts file
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css'],
})
export class TextAreaComponent implements OnChanges {
#Input() talksFromUser: string[] = [];
#Input() title: string = '';
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
}
My text-area.component.html
<div class="main-text-area">
<div *ngFor="let item of talksFromUser">{{ item }}</div>
</div>
custom-toolbar.component.ts file
import { Component, Input, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-custom-toolbar',
templateUrl: './custom-toolbar.component.html',
styleUrls: ['./custom-toolbar.component.css'],
})
export class CustomToolbarComponent implements OnInit {
talks: string[] = [];
#Input() handleSubmit!: (args: any) => void;
constructor() {}
ngOnInit(): void {}
onSubmit(f: NgForm) {
this.handleSubmit(f.value);
f.resetForm();
}
}
I tried also
this.talks = [...this.talks, data.talk]
Thank you all.
There are two issues in your code:
First one, you are calling handleSubmit("string") (so data is a string), but you are pushing data.talk, which is undefined (so talks will be [undefined, undefined, ...]). To fix it, use data:
handleSubmit(data: any): void {
this.talks.push(data); // use "data" instead of "data.talk"
}
Second one, you are using a AppComponent method into CustomToolbarComponent class. You need to keep the this scope of AppComponent. Also, you should use arrow functions:
handleSubmit = (data: any): void => {
this.talks.push(data);
}
import { NgForm } from '#angular/forms';
import { Exercise } from './../exercise.model';
import { TrainingService } from './../training.service';
import { Component, OnInit, ViewChild } from '#angular/core';
import { AngularFirestore } from 'angularfire2/firestore';
import { Observable, Subscriber } from 'rxjs';
import 'rxjs/add/operator/map';
#Component({
selector: 'app-new-training',
templateUrl: './new-training.component.html',
styleUrls: ['./new-training.component.css'],
})
export class NewTrainingComponent implements OnInit {
exercises: Observable<Exercise[]>;
constructor(
private trainingService: TrainingService,
private db: AngularFirestore
) {}
ngOnInit(): void {
this.exercises = this.db
.collection('availableExercises')
.snapshotChanges()
.map((docArray) => {
return docArray.map((doc) => {
return {
id: doc.payload.doc.data().id,
name: doc.payload.doc.data().name,
duration: doc.payload.doc.data().duration,
calories: doc.payload.doc.data().calories,
};
});
});
}
onStartTraining(form: NgForm) {
this.trainingService.startExercise(form.value.exercise);
}
}
Have issue with id, name, duration, and calories.. they are all underlined and error says: Property does not exist on type 'unknown' for all four. So not sure what the issue is. I have tried as well
id: doc.payload.doc['id'],
name: doc.payload.doc['name'],
duration: doc.payload.doc['duration'],
calories: doc.payload.doc['calories'],
doesn't work as well. Was repeating after Maximilian Schwarzmuller's tutorial. Would appriciate any help.
The answer was to change the next line on the function ngOnInit from:
return docArray.map((doc) => {
Into:
return docArray.map((doc: any) => {
This will allow you to allow you to avoid type checking during compilation. As mentioned here
I'm not sure how can I use a custom component if it's wrapper under another component.
Like:
ComponentA_withForm
|
--ComponentA1_withWrapperOfCustomInput
|
--ComponentA11_withCustomInput
if I have a structure like this:
ComponentA_withForm
|
--ComponentA11_withCustomInput
Everything's fine
But for my case (tons of async data) I need a wrapper... Is it possible somehow to do this?
Here is my fiddle code:
ComponentA:
import { Component } from '#angular/core';
import { FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
ComponentA1:
import { Component } from '#angular/core';
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input></custom-input>',
})
export class CustomInputWrapperComponent {
constructor() { }
}
ComponentA11:
import { Component, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `Hey there! <button (click)="inc()">Value: {{ value }}</button>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
}],
})
export class CustomInputComponent implements ControlValueAccessor {
private value = 0;
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
inc() {
this.value = this.value + 1;
this.onChangeFn(this.value);
}
onChangeFn = (_: any) => { };
}
And here I have a working sample:
https://stackblitz.com/edit/angular-qmrj3a
so: basically removing & refactoring code not to use CustomInputWrapperComponent makes my code working. But I need this wrapper and I'm not sure how to pass formControlName then.
I don't want a dirty solution with passing parent formGroup :)
Since you don't want a dirty solution ;) , you could just implement ControlValueAccessor in the CustomInputWrapperComponent also. That way any change in the parent will be reflected in the child, any change in the child will be reflected in the parent as well with just few lines of code.
Wrapper Component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="value"></custom-input>',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputWrapperComponent),
multi: true,
}]
})
export class CustomInputWrapperComponent implements AfterViewInit, ControlValueAccessor {
public value = new FormControl();
constructor() { }
ngAfterViewInit() {
this.value.valueChanges.subscribe((x) => {
this.onChangeFn(x);
});
}
writeValue(value: number): void {
this.value.setValue(value);
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
onChangeFn = (_: any) => { };
}
Parent Template
<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>
I have made a stackbitz demo here - https://stackblitz.com/edit/angular-csaxcz
you cannot use formControlName on custom-input-wrapper because it doesn't implement ControlValueAccessor. implementing ControlValueAccessor on custom-input-wrapper might be a solution but it seems to be overkill. Instead pass the control from formGroup to custom-input-wrapper as an #Input() and pass the inputed formControl to custom-input
app.component
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper [formCtrl]="form.get('someInput')"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
custom-input-wrapper.component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="formCtrl"></custom-input>',
})
export class CustomInputWrapperComponent {
#Input() formCtrl: AbstractControl;
constructor() { }
}
here is a working demo https://stackblitz.com/edit/angular-3lrfqv
I'm building an app
with component FormComponent.
inside I'm using reactive forms module from angular core
and create a custom validator.
the function is calling another function by using this - as I supposed it will refer to the FormComponent,
but it refers to be 'undefined'
(?)
The code in onInit defines the FormGroup and FormControl
and outside of it defines the functions
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
formInsurance:FormGroup;
private id:FormControl;
constructor(){}
ngOnInit() {
this.id = new FormControl('',[
Validators.required,
Validators.minLength(3),
Validators.maxLength(10),
Validators.pattern('^[0-9]+(-[0-9]+)+$|^[0-9]+$'),
this.foo
]);
this.formInsurance = new FormGroup({
id:this.id
})
}
foo(control:FormControl){
this.boo();
if(control.value){
return {
objToReturn: {
returned: name
}
};
}
return null
}
boo(){
console.log('boo')
}
}
The context in the foo method when called from within the FormControl is not referencing the FormComponent.
You can do the following to fix this behavior using bind to set the context yourself:
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
formInsurance:FormGroup;
private id:FormControl;
constructor(){}
ngOnInit() {
const id = new FormControl('',[
Validators.required,
Validators.minLength(3),
Validators.maxLength(10),
Validators.pattern('^[0-9]+(-[0-9]+)+$|^[0-9]+$'),
this.foo.bind(this)
]);
this.id = id;
this.formInsurance = new FormGroup({
id
})
}
foo(control:FormControl) {
this.boo();
if(control.value){
return {
objToReturn: {
returned: name
}
};
}
return null
}
boo(){
console.log('boo')
}
}
And of course, the other alternative is the arrow function, which auto-binds to the this context:
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators, ValidationErrors } from '#angular/forms';
import { Router } from '#angular/router';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
formInsurance:FormGroup;
private id:FormControl;
constructor() {}
ngOnInit() {
this.id = new FormControl('',[
Validators.required,
Validators.minLength(3),
Validators.maxLength(10),
Validators.pattern('^[0-9]+(-[0-9]+)+$|^[0-9]+$'),
this.foo
]);
this.formInsurance = new FormGroup({
id:this.id
})
}
foo = (control: FormControl): ValidationErrors => {
this.boo();
if (control.value) {
return {
objToReturn: {
returned: name
}
};
}
return null
}
boo() {
console.log('boo')
}
}
My http-data.service accepts json for output in the component template. Initially, the console shows that the first few calls are given undefined, and the following calls are already taking json, but also if you check the component, then the component shows that the method that outputs the data to the component is called only once and since the data has not yet arrived it writes undefined , But not updated after the arrival of json. Help please understand why? Thank you
My http-data.service:
import {Injectable} from '#angular/core';
import {Http} from '#angular/http';
import {Response} from '#angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
#Injectable()
export class HttpService{
constructor(private http: Http) {}
getDataOrganizations(): Observable<any[]>{
return this.http.get('http://localhost:3010/data')
.map((resp:Response)=>{
let dataOrganizations = resp.json().organization;
return dataOrganizations;
});
}
getDataModules(): Observable<any[]> {
return this.http.get('http://localhost:3010/data')
.map((resp: Response)=> {
let dataModules = resp.json().modules;
return dataModules;
});
}
getDataPresets(): Observable<any[]> {
return this.http.get('http://localhost:3010/data')
.map((resp: Response)=> {
let dataPresets = resp.json().presets;
return dataPresets;
});
}
getDataModuleItems(): Observable<any[]> {
return this.http.get('http://localhost:3010/data')
.map((resp: Response)=> {
let dataModuleItems = resp.json().module_items;
return dataModuleItems;
});
}
}
My data-all.service
import { Injectable, EventEmitter } from '#angular/core';
import {Response} from '#angular/http';
import { ModuleModel } from './model-module';
import { ModuleItemsModel } from './model-module-items';
import data from '../data/data-all';
import { PriceService } from './price.service';
import { HttpService } from './http-data.service';
#Injectable()
export class ModuleDataService {
constructor(private priceService: PriceService, private httpService: HttpService){
this.dataMinMaxSum = {minSum: 0, maxSum: 0}
}
private currentPopupView: EventEmitter<any> = new EventEmitter<any>();
private dataModules: ModuleModel[] = this.getDataModules();
private dataMinMaxSum: {};
private dataCalculateVariationOrg: any[];
private dataChangeExecutor: any[];
subscribe(generatorOrNext?: any, error?: any, complete?: any) {
this.currentPopupView.subscribe(generatorOrNext, error, complete);
}
calculte(){
return this.priceService.getDataPrice();
}
getDataModules(){
this.httpService.getDataModules().subscribe(((modules)=>{this.dataModules = modules; console.log(this.dataModules);}));
console.log('dataModules');
console.log(this.dataModules);
return this.dataModules;
}
---------------------------------------------------------------------------
}
My left-block.component
import { Component, OnInit} from '#angular/core';
import { ModuleDataService } from '../../service/data-all.service';
import { ModuleModel } from '../../service/model-module';
#Component({
moduleId: module.id,
selector: 'modules-left-block',
templateUrl: './modules-left-block.html',
styleUrls: ['modules-left-block.css']
})
export class ModuleLeft implements OnInit{
modules: ModuleModel[];
constructor(private modulesAll: ModuleDataService){}
ngOnInit(){
this.modules = this.modulesAll.getDataModules();
console.log("view");
console.log(this.modulesAll.getDataModules());
}
onToggle(module: any){
this.modulesAll.toggleModules(module);
}
}
My left-block.component.html
<div class="modules-all">
<div class="modules-all-title">Все модули</div>
<div class="module-item" *ngFor="let module of modules" [ngClass]="{ 'active': module.completed }" (click)="onToggle(module)">{{module?.title}}</div>
</div>
In the component this.modulesAll.getDataModules () method is why it is executed only once without updating (write in console => undefined), if there are any thoughts, write, thanks.
This behaviour is due to the .subscribe() method does not wait for the data to arrive and I'm guessing you already know this. The problem you're facing is because, you have .subscribe to the getDataModules() service in the wron place. You shouldn't subscribe to a service in another service (at leat in this case). Move the subscribe method to the left-block.component and it should work.
getDataModules() {
this.httpService.getDataModules().subscribe(((modules) => {
this.dataModules = modules;
console.log(this.dataModules);
}));
console.log('dataModules');
console.log(this.dataModules);
return this.dataModules;
}
It should look somethig like this:
#Component({
moduleId: module.id,
selector: 'modules-left-block',
templateUrl: './modules-left-block.html',
styleUrls: ['modules-left-block.css']
})
export class ModuleLeft implements OnInit {
modules: ModuleModel[] = new ModuleModel();
constructor(private modulesAll: ModuleDataService, private httpService: HttpService) {}
ngOnInit() {
this.getDataModles();
//this.modules = this.modulesAll.getDataModules();
console.log("view");
//console.log(this.modulesAll.getDataModules());
}
onToggle(module: any) {
this.modulesAll.toggleModules(module);
}
getDataModules(): void {
this.httpService.getDataModules().subscribe(((modules) => {
this.modules = modules;
console.log(this.dataModules);
}));
}
}