i'm getting a list from json which i want to display and being able to filter using an input box. so far it's working but the list only displays once i input something into the searchbox not before that. i would like the list being displayed at the start.I tried to set the search box value at start with empty string but it's not working.
looked up the web didn't find the way to achieve. Any help would be appreciated.
my component html:
<h4><label for="search-box">Hero Search</label></h4>
<input #searchBox id="search-box" (input)=search(searchBox.value) />
<ul class="heroes">
<li *ngFor="let hero of heroes$ | async">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">Delete</button>
</li>
</ul>
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
component ts:
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes$: Observable<Hero[]>=null;
heroes: Hero[];
selectedHero: Hero;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
service ts:
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return full array.
return this.getHeroes();
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
You can declare your searchTerms as an BehaviorSubject instead of an simple Subject. The main difference is that as an subcriber of that BehaviorSubject, you will get the last value emitted from that observable no matter when your subscription will happen. Be aware that BehaviorSubject need an initial value that will be emitted when you initialized it.
private searchTerms: BehaviorSubject<string> = new BehaviorSubject<string>('');
Because in your case it's connected to the input event, you need to automatically trigger a first value to start your pipeline.
You can do this by using the startWith operator
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
startWith(''), // <-- start your pipe with an empty string
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
Related
I'm new to Stackoverflow and to Angular. I'm writing a frontend app (an ecommerce) in Angular. I binded a click event to a button of a mat-menu in order to have the following behavior: when I click on the button I want to display the products of the category that has been clicked. For example, if I click the button "Make-up" I want that all the products with category "Make-up" will be displayed. The problem is that when I click on the button nothing is displayed on the browser, but I suppose that the logic of the app works because if I open the console of the browser the producs of the selected category are printed out.
product.component.html:
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="categoryFilter('Make-up')"> Make-up</button>
...
</mat-menu>
product.component.ts:
#Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css'] })
export class ProductComponent implements OnInit {
public products: Product[] = [];
public product!: Product;
constructor(private productService: ProductService) { }
ngOnInit() {
this.getProducts();
}
public getProducts(): void{
this.productService.getProducts().subscribe(
(response: Product[]) => {
this.products= response;
},
(error: HttpErrorResponse) => {
alert(error.message);
}
);
}
public categoryFilter(category: string): void{
this.productService.getProductsByCategory(category).subscribe(
(response: void) => {
console.log(response);
},
(error. HttpErrorResponse)=>{
alert(error.message);
}
)
} }
I think you have missed binding the response to a property, and then using that property to display the data on your browser.
I don't know the response type, for demo purposes let's say it's a string.
First, you'll need to create a property just like you created public product!: Product; we'll call it categoryName: string
In the click event, you'll have to bind this property with your response, hence it should look something like this:
public categoryFilter(category: string): void{
this.productService.getProductsByCategory(category).subscribe(
(response: string) => {
this.categoryName = response;
console.log(response);
},
(error. HttpErrorResponse)=>{
alert(error.message);
}
)
}
Now You'll have to bind that categoryName in your HTML so that it can be displayed. You can use Angular's text interpolation which uses curly brackets {{}} to display string values in the HTML.
Hence your HTML will look like this:
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="categoryFilter('Make-up')"> {{ categoryName }} </button>
...
</mat-menu>
I advise you to read Angular's Guide as you proceed further.
Angular's text interpolation: https://angular.io/guide/interpolation
In my frontend part of application use a method
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber)
which returns Observable. On my backend part, this method returns a List<String>.
My question is: how do I get to loop over the elements of this list of Strings?
If you have an observable you have to subscribe to it to get the actual value. Within subscribtion its up to you, here you can map or loop over your values.
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).subscribe(allowedCases => {
allowedCases.map(allowedCase => {
// your code here
});
});
If you are using this List<String> observable to show on the HTML part you can use a combination of async and *ngFor to get the desired result.
//in your html for example
<ul>
<li *ngFor="let item of (data$ | async)"> {{ item }} </li>
</ul>
//in your component
//usual angular stuff
export class MyComponent implements OnInit {
data$: Observable<String[]>;
constructor(private knowledgeMan: YourServiceInterface){}
ngOnInit() {
data$ = this.knowledgeMan.getUserAllowedCases(Item.ItemNumber);
}
}
If you are just doing this to compute some value you can do this following.
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).pipe(
flatMap(),
map(item => //do something with item here)
).subscribe();
If as you say, getUserAllowedCases returns string[] then you can do this:
this.knowledgeMan.getUserAllowedCases(Item.ItemNumber).subscribe(x => {
// assuming x is string[]
for (const item of x) {
// use item
}
});
For my app, the ItemDetailComponent is where info of an item will be displayed. I have a service that retrieves all items using promise. I use ActivatedRoute to retrieve the item ID from the url path, then run the service, get all items, then find the item with the ID retrieved above, and assign it to selectedItem variable.
Here is item-detail.component.ts:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
console.log('Inside promise', this.selectedItem)
})
console.log('Outside promise', this.selectedItem)
}
}
And here is item-detail.component.html template so I could display my item, just an example:
<div>
<h1>{{title}}</h1>
<div *ngIf="selectedItem">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app returns nothing but the title unfortunately. I then added the two console.log() commands and found out that the one outside of the promise as well as the html template are rendered before the promise is fulfilled, and no selectedItem is available at that time. How could I force the app to execute them only after the promise is resolved in order to have the selectedItem in place for displayed?
EDIT: I added a new line in the html template to examine further:
<div>
<h1>{{title}}</h1>
<div><label>Item ID 1: </label>{{selectedItem.itemId}}</div>
<div *ngIf="selectedItem">
<div><label>Item ID 2: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app displays "Item ID 1:" label but with no actual id there. The console shows me an error saying that "Cannot read property 'itemId' of undefined", again confirming that the whole template is rendered before promise resolved and is not re-rendered after data is loaded. So weird.
You could create a Resolver for the route that fetches the desired data.
https://angular.io/api/router/Resolve
https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html
Add a boolean variable in to your class like
private dataAvailable:boolean=false;
and in the subscription to the promise,make this true when the data is available
then((selectedItem) => {
this.selectedItem = selectedItem;
this.dataAvailable=true;
console.log('Inside promise', this.selectedItem)
})
and in the template render when the data is available
<div>
<h1>{{title}}</h1>
<div *ngIf="dataAvailable">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
It should do the trick
Update
ngOnInit() seems to be just a event handler hook - returning anything won't affect anything it seems. Hence my old answer will not work.
There are other workarounds like using *ngIf or putting it in routes etc. but I wish there was something like resolvePromise(): Promise hook that would put a condition on resolution before rendering.
This is instead of developers putting the boilerplate in every component.
Old answer
Most likely that is because you are missing return statement in the second then.
then((selectedItem) => {
this.selectedItem = selectedItem
console.log():
return selectedItem;//
}
Is it possible that the ChangeDetection is set to OnPush somewhere up the component tree?
If that is the case, the template does not automatically rerender, because nothing triggers the ChangeDetection for this component.
Look out for a Component with the setting changeDetection: ChangeDetectionStrategy.OnPush
#Component({
selector: 'example',
template: `...`,
styles: [`...`],
changeDetection: ChangeDetectionStrategy.OnPush
})
Also you already have a valid solution by using a Resolver you could check if this helps:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService,
// the reference to the components changeDetector is needed.
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
// this triggers the changedetection and the template should be rerendered;
this.changeDetectorRef.detectChanges();
console.log('Inside promise', this.selectedItem)
});
console.log('Outside promise', this.selectedItem)
}
}
Here is a great article about Angulars ChangeDetection: https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
For some time I have been researching if, and how to bind complex model to ngModel. There are articles showing how it can be done for simple data (e.g. string) such as this. But what I want to do is more complex. Let's say that I have a class:
export class MyCoordinates {
longitude: number;
latitude: number;
}
Now I am going to use it in multiple places around the application, so I want to encapsulate it into a component:
<coordinates-form></coordinates-form>
I would also like to pass this model to the component using ngModel to take advantage of things like angular forms but was unsuccessful thus far. Here is an example:
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" [(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="coordinates">Coordinates</label>
<coordinates-form [(ngModel)]="model.coordinates" name="coordinates"></coordinates-form>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
Is actually possible to do it this way or is my approach simply wrong? For now I have settled on using component with normal input and emitting event on change but I feel like it will get messy pretty fast.
import {
Component,
Optional,
Inject,
Input,
ViewChild,
} from '#angular/core';
import {
NgModel,
NG_VALUE_ACCESSOR,
} from '#angular/forms';
import { ValueAccessorBase } from '../Base/value-accessor';
import { MyCoordinates } from "app/Models/Coordinates";
#Component({
selector: 'coordinates-form',
template: `
<div>
<label>longitude</label>
<input
type="number"
[(ngModel)]="value.longitude"
/>
<label>latitude</label>
<input
type="number"
[(ngModel)]="value.latitude"
/>
</div>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CoordinatesFormComponent,
multi: true,
}],
})
export class CoordinatesFormComponent extends ValueAccessorBase<MyCoordinates> {
#ViewChild(NgModel) model: NgModel;
constructor() {
super();
}
}
ValueAccessorBase:
import {ControlValueAccessor} from '#angular/forms';
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
touch() {
this.touched.forEach(f => f());
}
}
Usage:
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
<coordinates-form
required
hexadecimal
name="coordinatesModel"
[(ngModel)]="coordinatesModel">
</coordinates-form>
<button type="Submit">Submit</button>
</form>
The error I am getting Cannot read property 'longitude' of undefined. For simple model, like string or number it works without a problem.
The value property is undefined at first.
To solve this issue you need to change your binding like:
[ngModel]="value?.longitude" (ngModelChange)="value.longitude = $event"
and change it for latitude as well
[ngModel]="value?.latitude" (ngModelChange)="value.latitude = $event"
Update
Just noticed you're running onChange event within settor so you need to change reference:
[ngModel]="value?.longitude" (ngModelChange)="handleInput('longitude', $event)"
[ngModel]="value?.latitude" (ngModelChange)="handleInput('latitude', $event)"
handleInput(prop, value) {
this.value[prop] = value;
this.value = { ...this.value };
}
Updated Plunker
Plunker Example with google map
Update 2
When you deal with custom form control you need to implement this interface:
export interface ControlValueAccessor {
/**
* Write a new value to the element.
*/
writeValue(obj: any): void;
/**
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any): void;
/**
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any): void;
/**
* This function is called when the control status changes to or from "DISABLED".
* Depending on the value, it will enable or disable the appropriate DOM element.
*
* #param isDisabled
*/
setDisabledState?(isDisabled: boolean): void;
}
Here is a minimal implementation:
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
// view => control
onChange = (value: T) => {};
onTouched = () => {};
writeValue(value: T) {
// control -> view
}
registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}
It will work for any type of value. Just implement it for your case.
You do not need here array like (see update Plunker)
private changed = new Array<(value: T) => void>();
When component gets new value it will run writeValue where you need to update some value that will be used in your custom template. In your example you are updating value property which is used together with ngModel in template.
In my example i am drawing new marker.
DefaultValueAccessor just updates value property https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L76
Datepicker in angular2 material is setting inner value as you do https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L202
When you need to propagate changes to AbstractControl you have to call onChange method which you registered in registerOnChange.
I wrote this.value = { ...this.value }; because it is just
this.value = Object.assign({}, this.value)
it will call setter where you call onChange method
Another way is calling onChange directly that is usually used
this.onChange(this.value);
Your example https://plnkr.co/edit/Q11HXhWKrndrA8Tjr6KH?p=preview
DefaultValueAccessor https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L88
Material2 https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L173
You can do anything you like inside custom component. It can have any template and any nested components. But you have to implement logic for ControlValueAccessor to do it working with angular form.
If you open some library such angular2 material or primeng you can find a lot of example how to implement such controls
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>