Tour of heroes Search component - html

I'm trying to add a div in the search component in Tour of Heroes angular 2 so, when the search component is resolving the request, a three dots appear. And, once the observable is resolved, the results are shown, or, if no results are present, a message like Not found is shown.
So far, I have this:
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngIf="searchBox.value.length > 0 && !(heroes | async)" >...</div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>
If you can see, I added the following div
<div *ngIf="searchBox.value.length > 0 && !(teams | async)" >...</div>
Trying to make the three dots to appear when the search box isn't empty and when the teams is not resolved yet.
But it is not working very well since, if I try to search for something, in the meantime the request is done, I can see the three dots but, once is resolved, if I removed some letters and try again, the three dots don't appear anymore.
This is the controller, it is exactly the same as the one you can find in the your of heroes (https://angular.io/docs/ts/latest/tutorial/toh-pt6.html)
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';
#Component({
moduleId: module.id,
selector: 'hero-search',
templateUrl: 'hero-search.component.html',
styleUrls: [ 'hero-search.component.css' ],
providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(
private heroSearchService: HeroSearchService,
private router: Router) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait for 300ms pause in events
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if no search term
: Observable.of<Hero[]>([]))
.catch(error => {
// TODO: real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}
gotoDetail(hero: Hero): void {
let link = ['/detail', hero.id];
this.router.navigate(link);
}
}
Do you know how can I improve that condition?

in your condition:
<div *ngIf="searchBox.value.length > 0 && !(heroes | async)" >...</div>
and you empty array when search returns no result.
but heroes empty array returns true
So you can either set heroes null or undefined
or check length instead, so try
<div *ngIf="searchBox.value.length > 0 && !((heroes&& heroes.length>0) | async)" >...</div>

Related

Angular: ERROR TypeError: Cannot read property 'choice_set' of null, while data displayed correctly

i hope you're doing well.
I am trying to implement a FormsBuilder in Angular by accessing the data from an API. The data is pushed down to its child-component via #Input().
However the data gets pushed down, are provided and shown successfully, but still I get this Error, when the first attempt from ngOnChangess tries to receive the data.
ERROR TypeError: Cannot read property 'choice_set' of null
at StyleTypeQuestionComponent.setFormValues (style-type-question.component.ts:34)
at StyleTypeQuestionComponent.ngOnChanges (style-type-question.component.ts:26)
at StyleTypeQuestionComponent.rememberChangeHistoryAndInvokeOnChangesHook (core.js:1471)
at callHook (core.js:2490)
at callHooks (core.js:2457)
at executeInitAndCheckHooks (core.js:2408)
at refreshView (core.js:9207)
at refreshEmbeddedViews (core.js:10312)
at refreshView (core.js:9216)
at refreshComponent (core.js:10358)
The data is provided through an data-service and are subscribed through an async pipe from its parent-component and as mentioned above pushed down via property binding.
I tried to use the ? operator in my template and tried to set an Timeout on the childcomponent. Also i tried to initialize the data via default values. Still thats making no sense for me right know, because the data is already available through his parent component and getting checked via an *ngIf directive.
I hope i could provided as much as information as needed.
I guess there is an initializing problem in the first seconds of ngChanges.
Parent-Component
import { Component, Input, OnChanges, OnInit } from '#angular/core';
import { Question } from '../shared/models/question';
import { QuestionStoreService } from '../shared/question-store.service';
import { Observable } from 'rxjs';
#Component({
selector: 'pc-style-type-detection',
templateUrl: './style-type-detection.component.html',
styleUrls: ['./style-type-detection.component.css'],
})
export class StyleTypeDetectionComponent implements OnInit, OnChanges {
question$: Observable<Question>;
#Input() question_Input: Question;
question_index: number = 1;
constructor(private qs: QuestionStoreService) {}
ngOnInit(): void {
this.question$ = this.qs.getSingle(1);
}
ngOnChanges(): void {}
updateBook(question: Question): void {
console.log(question);
}
}
Parent-Template
<pc-style-type-question
*ngIf="question$"
(submitQuestion)="updateBook($event)"
[question]="question$ | async"
></pc-style-type-question>
Child-Component
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
} from '#angular/core';
import { FormArray, FormBuilder, FormGroup } from '#angular/forms';
import { Choice, Question } from '../shared/models/question';
#Component({
selector: 'pc-style-type-question',
templateUrl: './style-type-question.component.html',
styleUrls: ['./style-type-question.component.css']
})
export class StyleTypeQuestionComponent implements OnInit, OnChanges {
questionForm: FormGroup;
#Input() question: Question;
#Output() submitQuestion = new EventEmitter<Question>();
constructor(private fb: FormBuilder) {}
ngOnChanges(): void {
this.initForm();
this.setFormValues(this.question);
}
ngOnInit(): void {
this.initForm();
}
private setFormValues = (question: Question) => {
this.questionForm.patchValue(question.choice_set);
this.questionForm.setControl(
'choice_set',
this.buildChoiceSetArray(question.choice_set)
);
};
initForm = () => {
if (this.questionForm) {
return;
}
this.questionForm = this.fb.group({
choice_set: this.buildChoiceSetArray([
{
choice_text: '',
choice_value: false,
},
]),
});
};
get choiceSet(): FormArray {
return this.questionForm.get('choice_set') as FormArray;
}
private buildChoiceSetArray = (values: Choice[]): FormArray => {
if (values) {
return this.fb.array(
values.map((choice) => {
return this.fb.control(choice.choice_value);
})
);
}
return this.fb.array(
this.question.choice_set.map((choices) =>
this.fb.control(choices.choice_value)
)
);
};
submitForm() {}
}
Child-Template
<form class="ui form" [formGroup]="questionForm" (ngSubmit)="submitForm()">
<div
formArrayName="choice_set"
*ngFor="let choiceset of choiceSet?.controls; index as i"
>
<div>
<input type="checkbox" [formControl]="choiceset" />
<label>
{{ question.choice_set[i].choice_text }}
</label>
</div>
</div>
</form>
Thank you in advance and wish you a nice weekend.
You are not using ngOnChanges the right way, its going to be triggered everytime your input change no matter what the value is which means you need to check if that value is what you expect it to be with SimpleChanges.
ngOnChanges(changes: SimpleChanges) {
if(changes.question.currentValue) {
this.initForm();
this.setFormValues(this.question);
}
}

Observable<any> Is not binding Angular 6

I have been working on another question here and the helper has gone a little quiet and I need to get a solution on this pretty quickly. See Here For More Information
I have implemented the new code and find that the array is returning 'false' to the browser:
I have mapped from the get request and then try bind commissions$ to the click-cards.component.html. This should then filter out any duplicate records and render them into groups using lodash.
Edits: based on feedback, but the result still seems to be the same
click-cards.component.ts
import { Component, OnInit } from '#angular/core';
import { Commission } from '../commission';
import { AnalyticsService } from '../analytics.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as _ from 'lodash';
#Component({
selector: 'app-click-cards',
templateUrl: './click-cards.component.html',
styleUrls: ['./click-cards.component.scss']
})
export class ClickCardsComponent implements OnInit {
commissions$: Observable<any>;
constructor(private analyticsService: AnalyticsService) {}
ngOnInit() {
this.getCommissions();
}
getCommissions(){
this.commissions$ = this.analyticsService.getAllCommissionData().pipe(
map((commissions: Commission[]) => {
if (commissions !== undefined && commissions !== null) {
return _.uniqBy(commissions, 'url');
}
}),
map((commissions: Commission[]) => {
commissions = _.groupBy(commissions, commission => commission.page_type);
return commissions;
})
)
}
}
I can't seem to find a way to get commissions$ to bind to the .html file:
click-cards.html
<ng-container *ngIf="commissions$ | async as commissions">
<ng-container *ngFor="let page_type of ['home', 'article','statistics', 'products']">
<h4>{{ page_type | titlecase }}</h4>
<p *ngIf="!commissions[page_type]">No {{ page_type }} Commissions Logged Yet</p>
<ul *ngFor="let card of commissions[page_type]">
<app-click-card [card]="card"></app-click-card>
</ul>
</ng-container>
</ng-container>
Does anyone know what I am doing wrong here? I don't usually work with Observables, so I normally subscribe to the service REST method and it works. So I am a little new to this process.
The map operator will allow you to transform the value of the observable source. Whatever you return there will replace the value. In the second map you return true so that's what the result value will be in the end. You should return the transformed commissions value again.

Change variable value from different component - Angular 4

I have two components, one is the home.component, and one is loading.component (loading screen). The loading screen has a countdown, and after the countdown, he does something. What I need to do is make the loading screen capable of custom countdown length.
I tried using the EventEmiter, but it doesn't work.
home.component.html:
<a (click)="applyChanges()" class="green" style="margin-left: 8px; cursor: pointer"><i style="margin-right: 5px;"class="fa fa-check"></i>Apply changes now</a>
home.component.ts
#Output() loadingScreenStart: EventEmitter<any> = new EventEmitter();
applyChanges(){
this.dashboard.applyChanges().catch(
err => {
console.log("There was an error: "+err.status);
console.log("There was an error: "+err.statusText);
//this.handleError(err);
return Observable.throw(err);
}
)
.subscribe(
data => {
this.loadingScreenStart.emit(34);
this.router.navigate(['/loading']);
}
);
}
loading.component.html
<div class="container container-table" (loadingScreenStarted)="onScreenStart($event)">
<div class="row vertical-10p">
<div class="container">
<img src="../../assets/img/LoginLogo.png" class="center-block logo">
<img style="width: 15em" src="../../assets/img/gears.svg" class="center-block ">
<div class="text-center col-sm-6 col-sm-offset-3">
<h1>Changes are taking place</h1>
<h4>You will be redirected in {{timeLeft}} seconds</h4>
</div>
</div>
</div>
loading.component.ts
import { Component, OnInit, Input } from '#angular/core';
import { AuthGuard } from '../guard/auth.guard';
import { Title } from '#angular/platform-browser';
#Component({
selector: 'app-loading',
templateUrl: './loading.component.html',
styleUrls: ['./loading.component.css']
})
export class LoadingComponent implements OnInit {
public timeLeft;
public intervalId;
constructor(public titleService: Title, public guard: AuthGuard) { }
onScreenStart(length) {
this.timeLeft = length;
console.log(length);
}
ngOnInit() {
this.titleService.setTitle("Loading... | something);
this.startCounter();
localStorage.removeItem('statusInfo');
}
startCounter() {
this.intervalId = setInterval(() => {
if (this.timeLeft == 1) {
clearInterval(this.interValId);
}
this.timeLeft--;
}, 1000)
}
}
There are 2 things wrong in you code.
Understand how EventEmitter works.
When you are emitting a value from child using eventEmitter, you will need to map it to some method of the parent component.
// in child component.
#Output()
myEmitter = new EventEmitter();
someMethod() {
this.myemitter.emit('somevalue');
}
Now it actually receive this value of emitter you need to map this to a method of parent compoenent.
someParentMethod(value) {
console.log(value);
}
<app-child (myEmitter)="someParentMethod(event)"></app-child>
2. Observable's error handling.
Since you are throw an Observable, you will need to handle it in the subscribe. Observable's subscribe consist of 3 parts. Response, ErrorHandling, Next. Now, you are throwing an error from Observable.throw, your res part will not run. But you are not handling error, so you won't see anything. Instead do this:
.subscribe(
data => {
this.loadingScreenStart.emit(34);
this.router.navigate(['/loading']);
},
error => console.log(error)
);
Since these two components are not in a parent/child relationship, I don't think that you're going to be able to successfully use an EventEmitter. The major problem is that you can't depend on both components being in existence at the same time.
So, the two common choices for your situation are:
pass the data in a route parameter (example at https://angular.io/tutorial/toh-pt5#navigating-to-hero-details)
communicate through a shared service (example at https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service)

Angular5 *ngfor not working, but there is no error

I'm facing a problem with Angular at the moment.
I want to read data from my server API and want to display it with *ngfor in a html document.
I can receive the data from the API, but i can't display it.
I took the example code from the tour of heroes tutorial and changed it:
The data gets through to my angular app. I can console.log it and see it in chrome development console.
I tried to display other data that I get from my api and it is working. You can see the data commented out in heroes.components.ts.
Who can help me with this?
If you want to see some more of the code like imports please tell me. But i guess everything needed imported as there are no error messages, i can get the data from my api and i can display some data (sadly not the data i need).
I tried several ideas to solve this from some other posts, but can't get it working.
Here are some Code Snippets:
This is my hero.service.ts
imports...
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { HttpResponse } from '#angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from '../model/hero';
import { MessageService } from '../message.service';
import { Response } from '#angular/http/src/static_response';
getHeroes(): Observable<Hero[]> {
console.log("GET HEROES IN HEROES.SERVICE");
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(Hero => console.log(`fetched heroes: `)),
catchError(this.handleError('getHeroes', []))
);
//I also tried to just use return this.http.get<Hero[]>(this.heroesUrl);
This is my
heroes.components.ts
import { Component, OnInit } from '#angular/core';
import { Hero } from '../../model/hero';
import { HeroService } from '../hero.service';
import { CommonModule } from '#angular/common';
import { Pipe } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Response } from '#angular/http/src/static_response';
// For use of map
import 'rxjs/Rx';
#Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Observable<Hero[]>;
// I tried to display some data
// heroes: any[] = [
// {
// "name": "Douglas Pace"
// }
// ];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
// undefined
console.log("ONINIT");
console.log(this.heroes);
}
getHeroes(): void {
console.log("GET HEROES IN HEROES.COMPONENT");
this.heroService.getHeroes()
.subscribe(
function(response: Hero[]) {
console.log("RESPONSE IN HEROES COMPONENT");
console.log(this.heroes);
var res = response["data"];
// console.log(res.json());
this.heroes = res;
console.log(this.heroes);
console.log(response["data"]);
},
function(error) {
console.log("Error happened" + error)
},
function() {
console.log("the subscription is completed")
//This shows me the right data.
console.log(this.heroes[5].id);
console.log(this.heroes[5].titel);
console.log(this.heroes[5].name);
console.log(this.heroes[5].vorname);
}
);
}
My html file:
<h2>My Heroes</h2>
<!-- <input type=text ng-model="hero"> -->
// I gave it a try with and without *ngIf="heroes"
<!-- only show the list IF the data is available -->
<div *ngIf="heroes">
<h3>Heroes are available and are displayed</h3>
<li *ngFor="let hero of heroes">
{{hero.name}}
</li>
</div>
<button (click)="button()">
Suchen
</button>
<div *ngIf="heroes">
<table class="heroes">
<tr>
<th>Id</th>
<th>Titel</th>
<th>Nachname</th>
<th>Vorname</th>
</tr>
//I tried async as the data is maybe not available from the
beginning. Also tried async on hero as heroes is created on init
and single heros are added with the function getHeroes();
<tr *ngFor='let hero of heroes | async'>
<a routerLink="/detail/{{hero.id}}">
<td>{{hero.id}}</td>
<td>{{hero.titel}}</td>
<td>{{hero.name}}</td>
<td>{{hero.vorname}}</td>
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</tr>
</table>
</div>
<pre>{{heroes | json}}</pre>
If got a hero interface. Should be my model. Only Last and First name are needed.
export interface Hero {
id?: string;
name: string;
titel?: string;
vorname: string;
}
The JSON I returned from my API. Online Json formatter says it is valid json.
{"status":"Success","data":
[{"id":"36","name":"Hero","vorname":"Super","titel":"Dr.",},
{"id":"34","name":"Man","Spider":"Ines","titel":""}],
"message":"Retrieved all HEROES"}
this.heroService.getHeroes()
.subscribe(
function(response: Hero[]) { }
Your problem could be here. Your response is an object with (let's say, interface):
interface DataResponse {
success: string;
data?: Hero[];
}
Because you set response: Hero[] and there's no data property in your Hero interface, response["data"] returns null and you'll never get your data. If you run response.data, you'll probably get an error saying data is not defined in Hero etc...
Change to the following:
this.heroService.getHeroes()
.subscribe((response: DataResponse) => {
this.heroes = response["data"];
});
Your code seems to be ok but i see an error in your json format here
"titel":"Dr.",},
try to remove the comma after Dr and give it a try
"titel":"Dr."},

Angular2 Auto suggester

I have been searching a lot on INTERNET but still unable to figure out how i can make custom auto suggester without any third party. After a lot of google I found this
but the issue is that my response from api is a bit different i am getting response as :
[{"id":"1","name":"aa"},{"id":"2","name":"bb"}...]
due to which i am getting [object][object] as value in pipe.
Can anyone please help how i can handle this request with this pipe. I want that their should be a text box on whose click there should be listing and on user input the below suggestions may vary.
Pipe:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'FilterPipe',
})
export class FilterPipe implements PipeTransform {
transform(value: any, input: string) {
if (input) {
input = input.toLowerCase();
return value.filter(function (el: any) {
return el.toLowerCase().indexOf(input) > -1;
})
}
return value;
}
}
in ts:
import { Component } from '#angular/core';
import {FilterPipe} from './pipes'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title:String;
names:any;
constructor(){
this.title = "Angular 2 simple search";
this.names = ['Prashobh','Abraham','Anil','Sam','Natasha','Marry','Zian','karan']
}
}
*** this works perfectly but in my case this.name array is deferent as told above.
Given the fact that the data source is an array of objects, to be dynamic I will use the following pipe that will iterate each object for values, then if a match is found will keep the object for display:
Pipe
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'filter',
pure: false
})
export class FilterPipe implements PipeTransform {
transform(items: Array<any>, filter: string): any {
if (!filter) {
return items;
}
return items.filter(item => {
for (let field in item) {
if (item[field] === null || item[field] === undefined){
continue;
}
if (item[field].toString().toLowerCase().indexOf(filter.toLowerCase()) !== -1) {
return true;
}
}
return false;
}
);
}
}
HTML
<input type="text" [(ngModel)]="search" placeholder="Filter...">
<div *ngFor="let item of datasource | filter:search"></div>
Look at pure: false declaration of the pipe. This tells the pipe to filter continuously the data, so if you have a service that is dynamically pushing data into your datasource, your filtered display will update automatically. Also using a pipe like this you can filter on all values of your objects and objects can change structure dynamically without impact on your filter.
You can try something similar like below
<input type="text" [(ngModel)]="queryString" id="search" placeholder="Search to type">
<ul>
<li *ngFor="let n of list | FilterPipe: queryString : searchableList ">
{{n.name}} ({{n.id}})
</li>
</ul>
pass required field to be searched
this.searchableList = ['name','id']
pipe
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'FilterPipe',
})
export class FilterPipe implements PipeTransform {
transform(value: any, input: string,searchableList : any) {
if (input) {
input = input.toLowerCase();
return value.filter(function (el: any) {
var isTrue = false;
for(var k in searchableList ){
if(el[searchableList[k]].toLowerCase().indexOf(input) > -1){
isTrue = true;
}
if(isTrue){
return el
}
}
})
}
return value;
}
}
You can update the pipe according to your needs.