At first glance problem looks really easy, but unfortunately I have a problem with covering test scenario when nullable getter is null. Considering the sample below:
#Component({
selector: 'sample-form-test',
templateUrl: './sample-form-test.component.html'
})
export class SampleFormTestComponent implements ngOnInit {
form?: FormGroup;
get name(): FormControl | null {
return this.form?.get('name') as FormControl;
}
constructor(private formBuilder: FormBuilderService) {}
ngOnInit() {
this.form = this.formBuilder.build();
}
It is always underlined in a code coverage report that the question mark inside the getter above is not covered by the tests. Even if I try to cover it by checking directly whether is that getter null right after a component creation:
describe('create SampleFormTestComponent', () => {
let spectator = Spectator<SampleFormTestComponent>; // from #ngneat/Spectator
const createComponent = createComponentFactory({ // from #ngneat
component: SampleFormTestComponent,
providers: MockProvider(FormBuilderService, {
build: () => {
new FormGroup({name: new FormControl})
}
}
});
it('should create', () => {
spectator = createComponent();
expect(spectator).toBeTruthy();
expect(spectator.component.name).toBeNull();
});
}
It doesn't help at all and branch coverage is always lowered by that nullable getters. The worst scenario is when I have them multiple inside the class - in that scenario it is not possible to meet code coverage requirements. Do you know how can I easily solve that problem?
UPDATE
The easiest way is to initialize form in a constructor – it will allow to access the form properties (even if those are empty) without necessity to check if those values are null ot not.
#Component({
selector: 'sample-form-test',
templateUrl: './sample-form-test.component.html'
})
export class SampleFormTestComponent implements ngOnInit {
form: FormGroup;
get name(): FormControl {
return this.form.get('name') as FormControl;
}
constructor(private formBuilder: FormBuilderService) {
this.form = this.formBuilder.build();
}
Here is the updated Stackblitz solution
Related
I'm here because I do not understand how Http works in angular. I would create a "news" thread on my website. To do that I have created a service in my angular app that calls a .net core web API.
Also, I would add a paginate to my thread (I want to display news by 5 on the page).
I can get my values, that is not my issue here. But, to create my paginate, I need to have values for number of pages calculation.
I tried to add code to create my paginate (number of pages, number of elements...) but I always get 0 to these values and my array of news is filled after the onInit(). This is what I don't understand.
This is my component:
import { Component, OnInit, OnDestroy } from '#angular/core';
import { NewsService } from '../news.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
title = 'News';
news = [];
displayed = [];
numberOfPages = 0;
constructor(private newsService: NewsService) { }
ngOnInit() {
// I don't really understand these lines (mainly the subscribe part)
this.newsService.getAllNews().subscribe((data) => {
this.news = Array.from(Object.keys(data), k => data[k]);
// this console.log appears after the onInit(), why ?
console.log(this.news);
});
this.numberOfPages = this.news.length / 5; // Get 0 here, why ?
}
}
My service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class NewsService {
private finalData = [];
private apiUrl = 'https://localhost:5001/api/v1/posts';
constructor(private http: HttpClient) { }
getAllNews() {
return this.http.get(this.apiUrl);
}
}
In the browser console, I get this:
console screen
Maybe I forgot something in my code or I don't know what.
Someone can help me to achieve my goal? I want to understand how to proceed to make a working paginate for my news.
You should add
this.numberOfPages = this.news.length / 5;
inside the subscribe
this.newsService.getAllNews().subscribe((data) => {
this.news = Array.from(Object.keys(data), k => data[k]);
// this console.log appears after the onInit(), why ?
console.log(this.news);
});
like so:
this.newsService.getAllNews().subscribe((data) => {
this.news = Array.from(Object.keys(data), k => data[k]);
// this console.log appears after the onInit(), why ?
console.log(this.news);
this.numberOfPages = this.news.length / 5;
});
My guess is that when you try to initialise the this.numberOfPagesthe this.news.length is not yet set(data are not yet retrieved from the API). Hope this helps
i have a service that returns an API response of type json, in this json object i have a list of number values.i can output those values on my webpage, but i would like to store the values in an array first to do some calculations on. i have tried many ways without success. please guide me
API response screenshot in postman
http call service
getTriggerCount():Observable<Trigger>{
return this.http.get(this.triggersUrl).pipe(
flatMap(count => transformAndValidate(Trigger, count)))
component
#Component({
selector: 'app-triggers',
templateUrl: './triggers.component.html',
styleUrls: ['./triggers.component.css']
})
export class TriggersComponent implements OnInit {
#Input() trigger: Trigger;
constructor(private triggerService: DbApiService) { }
ngOnInit() {
this.getTriggerCount();
}
getTriggerCount(){
this.triggerService.getTriggerCount() .subscribe(trigger => this.trigger = trigger);
}
}
Trigger Class
import { IsNumber, IsNotEmpty, IsString } from 'class-validator';
export class Trigger {
#IsNotEmpty()
#IsNumber()
result: number[];
constructor() { }
}
The issue is with your service, it should be like the following
getTriggerCount():Observable<any>{
return this.http.get(this.triggersUrl).map(res => res.json());
}
Using the new HttpClient it should like like this:
getTriggerCount():Observable<any>{
return this.http.get<any>(this.triggersUrl);
}
To make use of this in the component. You also do not need the #Input() for trigger. Your Trigger class is also over complicated for what you are doing. See below
public trigger: any;
getTriggerCount(){
this.triggerService.getTriggerCount()
.subscribe(trigger => this.trigger = trigger);
}
This will then have the response on the trigger object. If you want to make use of the object, to say, add all the numbers together, you would do the following:
addArray() {
let sum = this.trigger.reduce((a, b) => +a + +b, 0);
}
The +a and +b is to convert the item to a number. This won't work if the item can't convert to a number.
This question already has answers here:
How do I return the response from an Observable/http/async call in angular?
(10 answers)
Closed 5 years ago.
I have the following component:
import { Component, Input, OnInit } from '#angular/core';
import { ActivatedRoute, Params } from '#angular/router';
import 'rxjs/add/operator/switchMap';
import { ArticleStore } from '../../state/ArticleStore';
import { Article } from '../../models/article';
#Component({
selector: 'app-article-detail',
templateUrl: './article-detail.component.html',
styleUrls: ['./article-detail.component.css']
})
export class ArticleDetailComponent implements OnInit {
private article: Article;
constructor( private route: ActivatedRoute, private articleStore: ArticleStore ) { }
ngOnInit(): void {
this.route
.queryParamMap
.map((paramMap => paramMap.get('id') || 'None'))
.switchMap((id: string) => this.articleStore.getArticle(id))
.subscribe((article: Article) => {
this.article = new Article(article);
console.log(this.article) // <--returns full-filled object
});
console.log(this.article) // <-- undefined object
}
}
Inside the subscribe function, I get the proper object (this.article) and is what I expect. If I move down to after the this.route, it doesn't work. Should be straight forward to get the value assigned.
The whole project is here => https://github.com/flamusdiu/micro-blog
Edit
Kinda similar to How do I return the response from an Observable/http/async call in angular2?
I understand the async nature of the calls (actually more calls now-a-days are async).
When you nav to article/:id, it fires off the getArticle(id) function from the ArticleStore.ts
public getArticle (id: string) {
return this.pouchdbService.getArticle(id)
.then((res) => {return res.docs[0] });
}
This runs just fine. It pulls from my service:
public getArticle(id: string): Promise<any> {
return this._pouchDb.find({
selector: {_id: id }
});
}
In your application routes you should have something like:
{ path: '/articles/:id', component: ArticleDetailComponent},
Then your router will be able to act on the provided article route.
Also consider using a Resolver for getting data for the component before it is initialized. good luck with the blog :D
In my angular 2 application there is a component containing an array of objects that is passing the chosen (clicked) one to it's direct child component. This does display the data more detailed. I'm using the "SimpleChanges" feature to watch in this child component if the object given changed to make another http request to get the related comments from a database.
If I try to build it with npm I get an error, saying :
app/displayEntry.component.ts(23,41): error TS2339: Property 'entry' does not exist on type 'SimpleChanges'
If I just comment this part out, start npm and finally put it in there again and save it, there is no Problem anymore ( no erro and it works ).
My question is, is there a way to work around this behavior and can this cause any trouble later I don't foresee or should I just ignore it? Thanks for your help
Parent component:
import { Component, OnInit } from '#angular/core';
import { entry } from './Objekte/entry';
import { entryService } from './entry.service'
#Component({
templateUrl: 'app/Html_Templates/displayLastEntrys.template.html'
})
export class displayLastEntrys implements OnInit{
public entrys : entry[];
private entryChoosen: boolean;
private ChoosenEntry : entry;
constructor ( private entryservice : entryService){
this.entryChoosen = false;
}
ngOnInit() : void {
this.getData();
}
getData() {
this.entryservice.getFirstEntrys().then(entrys => this.entrys = entrys);
}
entryClicked(ent: entry){
this.entryChoosen = true;
this.ChoosenEntry = ent;
}
leaveEntry () {
this.entryChoosen = false;
}
voted( upordown : boolean ) {
}
}
Child component:
import { Component, Input, Injectable, OnChanges , SimpleChanges, Output, EventEmitter} from '#angular/core';
import { entry} from './Objekte/entry';
import { entryService } from './entry.service';
import { comment } from './Objekte/comments';
#Component({
selector: 'display-entry',
templateUrl: 'app/Html_Templates/displayEntry.template.html'
})
export class displayComponent implements OnChanges{
#Input() public entry : entry;
public comments : comment[];
private changecounter : number;
constructor(private service : entryService) {
this.changecounter = 0;
}
ngOnChanges(changes : SimpleChanges){
this.service.getComments(changes.entry.currentValue.id)
.then(com => this.comments = com )
.catch();
this.entry.viewed++;
// To implement :: change database
}
votedUp () : void {
this.entry.votes ++;
// To implement :: change database
}
votedDown () : void {
this.entry.votes --;
// To implement :: change database
}
}
The accepted solution is suboptimal for TypeScript, as you're defeating the type system.
SimpleChanges does not have an entry property, so the compiler quite rightly balks. The solution is to treat the changes object as an array:
ngOnChanges(changes : SimpleChanges){
if (changes['entry']) {
this.service.getComments(changes['entry'].currentValue.id)
}
}
Then you can continue to strongly type the ngOnChanges method.
To make the compiler not complain just change your method definition for parameter one from SimpleChanges to any:
ngOnChanges(changes: any) {
//...
Maybe it's changed a lot now but this works these days
import {Component, Input, OnChanges, SimpleChanges} from '#angular/core';
import {ConfigModel} from './config.model'
#Component({
selector: 'selector',
templateUrl: './template.html',
styleUrls: ['./styles.scss']
})
export class BlaComponent implements OnChanges {
#Input() config: ConfigModel;
ngOnChanges(changes: SimpleChanges): void {
if (changes.config && changes.config.currentValue) {
let config = <ConfigModel>changes.config.currentValue;
// do more
}
}
}
I myself got the compile error because i wasn't using .currentValue after calling changes.config
If you are completely dependent on the IDE's auto-completion, make sure to actually use SimpleChanges instead of just SimpleChange. A very thing to be overlooked at.
All the tutorials and answers that I have found show only how to pass a variable from parent component to child component using inputs but what is this child component is contained within the router outlet and not directly in the parent template ??
e.g:
Main component
#Component({
selector: 'my-app',
template: `
Main page
<router-outlet></router-outlet>
`,
directives: [ROUTER_DIRECTIVES]
})
#RouteConfig([
{ path: '/contact', name: 'Contact', component: ContactComponent},
])
export class AppComponent{
public num:Number = 123;
}
#Component({
selector: 'contact-page',
template: 'contact page'
})
export class ContactComponent{
public num:Number;
}
So in this example the main component template contain a router outlet where the child contact component will be rendered but how can I get variable "num" value in the child component evaluated inside a router outlet from the parent app component ??
I just stumbled over this question, here is how I have solved similar issues.
I would use a service to solve this. Then it is possible for all children and the parent to set the property, and the changes are propagated out for all subscribers. First I would create a service with a private BehaviorSubject which have a public getter and setter, to encapsulate ReplaySubject and only return Observable:
private _property$: BehaviorSubject<number> = new BehaviorSubject(1);
set property(value: number) {
this._property$.next(value);
}
get property$(): Observable<number> {
return this._property$.asObservable();
}
The reason for using new BehaviorSubject(1), is to set the initial value to 1, so there is something to subscribe to.
In the parents onInit, I would se the default value of property (num):
private _propertySubscribtion: Subscription
ngOnInit() {
// set default value to 5
this._componentService.property = 5;
// If property is updated outside parent
this._componentService.property$.subscribe(p => {
this.property = p;
});
}
ngOnDestroy() {
this._propertySubscribtion.unsubscribe();
}
In one or more of of the child components, it possible to subscribe for changes:
private _propertySubscribtion: Subscription
ngOnInit() {
this._propertySubscribtion = this._componentService.property$.subscribe(p => {
this.property = p;
});
}
ngOnDestroy() {
this._propertySubscribtion.unsubscribe();
}
And if some child or parent updates the property:
updateProperty() {
// update property
this._componentService.property = 8;
}
All subscribers will know about it.
Currently you can't bind to components added by the router. Use a shared service instead (there are tons of examples here on SO already) to pass data from or to the added component or to subscribe to events.
See also this issue https://github.com/angular/angular/issues/4452 especially this comment https://github.com/angular/angular/issues/4452#issuecomment-153889558
If your data is immutable, you can use RouteData.
#Component({...})
#RouteConfig([
{ path: '/contact', name: 'Contact', component: ContactComponent, data: {num: 123}},
])
export class AppComponent {
}
#Component({...})
export class ContactComponent {
public num:Number;
constructor(data: RouteData) {
this.num = data.get('num');
}
}
Could be useful to pass some configuration options you don't want to hardcode in child routes. But if you're expecting data to change, you'll need some other method.