I have an Angular 2 component that displays a list of Items, and that registers to a service that publishes events. The problem is that even if I don't do anything when I receive an event, Angular updates the view (or at least do something when, I guess, it shouldn't).
Here is a plunker.
As you can see in the console, the "getTitle()" method of my items is called each time my service publishes a message.
Even if I don't register to my service and if my component doesn't implement the MyServiceListener interface, getTitle is called each time the service gets a message. If I don't give the service to my component in its constructor, everything is fine. So, something seems wrong with my dependency injection, but what?
Here is the relevant code of the plunker:
My service and its listeners interface:
export interface MyServiceListener {
onMessage(_message: any);
}
export class MyService {
private m_listener: MyServiceListener;
constructor() {
window.setInterval(() => {
if (this.m_listener !== undefined) {
this.m_listener.onMessage("Hi");
}
}, 500);
}
setListener(_listener: MyServiceListener) { this.m_listener = _listener; }
}
The Item class:
export class Item {
m_title: string;
constructor(_title: string) {
this.m_title = _title;
}
getTitle(): string { console.log("getTitle"); return this.m_title; }
}
My component:
#Component({
selector: 'my-app',
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
export class App implements TestBugAngularServiceListener {
private m_items: Array<Item> = new Array<Item>();
constructor(_communicationService: MyService) {
this.m_items.push(new Item("A"));
this.m_items.push(new Item("B"));
this.m_items.push(new Item("C"));
_communicationService.setListener(this);
}
onMessage(_message: any) {
}
}
bootstrap(App, [MyService]).catch(err => console.error(err));
Both articles : Change detection and Angular immutability explain a lot of thing about how Angular 2 detect changes of object, and how the tree of components in angular 2 is traversed to perform data binding...
In your sample, I think your component "my-app" can be considered to be "Immutable", so changing its "change detection strategy" to OnPush solve your problem.
You can write this :
#Component({
selector: 'my-app',
changeDetection: ChangeDetectionStrategy.OnPush,
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
And after adding the import to ChangeDetectionStrategy, databinding of "my-app" will not be computed after each browser event, but only when its input change, so never...
Related
Basically I have an api that returns data when I do a search, I push them into an array and I display them by doing an ngFor in my html.
When I want to do a new search it's the same function that is called, but the html is not updated while I get new data.
It always appears the old data recovered the first time.
To search, i used this code :
SearchBar.component.ts
export class RechercheToutComponent implements OnInit {
searchInput = new FormControl('');
constructor(
private router: Router,
private recherche: RechercheComponent
) { }
ngOnInit(): void {
}
search() {
if(this.router.url.match(/recherche.*!/)){
this.recherche.searchResult(this.searchInput.value)
}else{
this.router.navigate(['/recherche'], {queryParams: {search: this.searchInput.value}}).then(r => this.recherche.searchResult(this.searchInput.value))
}
}
}
SearBar.component.html
<form class="catalogue-search-form" (ngSubmit)="search()">
<div class="search-bar">
<input type="text"
[formControl]="searchInput"
placeholder="Rechercher dans Intra"
/>
<button type="submit" class="text-button">
<mat-icon>search</mat-icon>
</button>
</div>
</form>
Search.component.ts
export class RechercheComponent implements OnInit {
searchParam: any;
results$: Observable<Array<any>>;
isResultLoading: boolean = true;
constructor(
private route: ActivatedRoute,
private http: HttpClient
) {
}
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.searchParam = params['search']
});
this.searchResult(this.searchParam);
}
searchResult(searchParam) {
this.http.get(`${environment.apiUrl}/Recherchetout.php`, {params: {search: searchParam}}).subscribe(
(data: Array<any>) => {
this.results$ = of(data)
this.isResultLoading = false;
}
);
}
}
Search.component.html
<div class="recherche">
<div class="spinner-search" *ngIf="isResultLoading">
<app-spinner></app-spinner>
</div>
<div class="content" *ngIf="!isResultLoading">
<div *ngFor="let oui of results$ | async">
<div *ngIf="oui.produit != undefined">
{{ oui.produit.pdf }}
</div>
</div>
</div>
</div>
I tried to create observables but it didn't work, with a simple array too.
So my question is: Why is my data not updating on my html?
And how to do it ?
Sorry in advance for the mistakes, or for the disastrous code I begin in angular
you are injecting RechercheComponent inside the SearchBar component, angular will create different instance than the one used on the UI.
to send data between multiple components create a parent component and use it to allow communication between the two components (use the Input and Output attributes).
make your system navigate to the parent then
Parent.Component.ts
export class ParentComponent {
data: any[];
isResultLoading = false;
updateData(data: any[]) {
this.data = data
}
}
parent.component.html
<app-search-bar (change)="dataChange($event)" [(isResultLoading)]="isResultLoading"></app-search-bar>
<app-search [data]="data" *ngIf="!isResultLoading"></app-search>
update your Search.component.ts
//add outputs and inputs
#Output() change = new EventEmitter<any[]>(); //make sure to import EventEmitter from #angular/core
#Input() isResultLoading : boolean
// update searchResult subscription function
searchResult(searchParam) {
this.isResultLoading = true;
this.http.get(`${environment.apiUrl}/Recherchetout.php`, { params: { search: searchParam } }).subscribe(
(data: Array<any>) => {
this.change.emmit(data);
this.isResultLoading = false;
}
);
}
and finally instead of having observable result$ inside Search.component.ts
replace it with #Input() data:Any[]
Assuming you want to refresh the search every time a URL change, update you ngOnInit by moving this.searchResult(this.searchParam); inside the subscribe method
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.searchParam = params['search']
this.searchResult(this.searchParam);
});
}
with this searchResult will be called every time the router parameters get change instead of updating for the first time ngOnInit get called
I am completely new to frontend dev and trying to display API data in an Angular 6 application and can't figure out how to do it.
I can display values in the top level of the returned details but it's the sub level details I am struggling with.
I am using an Angular 6 app using Routing.
Below is all my code
Homepage.component.html
<h2>Book ID List</h2>
<button (click)="getBooks()">Get</button>
<div *ngFor="let book of books.items">
<p>ID: {{book.id}}</p>
</div>
I can get the 'ID'
I am using a service to get the data from the test API
Service.component.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class ApiServiceService {
url = 'https://www.googleapis.com/books/v1/volumes?q=HTML5 Wire-frames';
constructor(private http: HttpClient) { }
private extractData(res: Response) {
const body = res;
return body || {};
}
getBooks(): Observable<any> {
return this.http.get(this.url).pipe(
map(this.extractData));
}
}
Homepage.component.ts
import { Component, OnInit } from '#angular/core';
import { ApiServiceService } from '../../services/api-service.service';
#Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
books: any = [];
constructor(private apiService: ApiServiceService) { }
ngOnInit() { }
getBooks() {
this.books = [];
this.apiService.getBooks().subscribe((data: {}) => {
console.log(data);
this.books = data;
});
}
}
At present this return the following:
What I want to do is display the value from the 'eBook' which is under the 'saleInfo' level. I know I need to the change the loop for each array returned in the HTML but this is where I'm stuck.
Also I'm not sure the code I have is the best, but it's working. As I said I'm new to all this, and can pull values from top level but not sublevels in the API.
I would recommend better naming for your service, Service.compoent.ts isn't ideal, api.service.ts is much more understandable.
Also you can see that when you subscribe, you are using data: {}, this means that the function should expect a value of type Object, but I would use any, since you use Observable<any>
Now for the problem.
I have created stackblitz which does just what you wanted. I think you have got confused with the comments. You don't want to change let book of books.items to let book of books because you would be iterating over object, which you cannot do in *ngFor.
Change the line this.books = data; to this.books.push(data);
Since, if it is this.books = data; and because the books is of type any. It will accept anything. So, now after this line, this.books = data; it becomes object which contains value of data variable. So, you should use,
this.books.push(data);
To make it behave like an array too. Then, you can access books with *ngFor.
So, now in the HTML you can access via *ngFor as:
<div *ngFor="let book of books">
<div *ngFor="let items of book.items">
<p>ID: {{items.id}}</p>
<p>ebook: {{items.saleInfo.isEbook}}</p>
</div>
</div>
I've jumped for a while from Angular 6 to Angular JS and I'm trying to code in Component Architecture. The problem is - I'm trying to use ng-show="somevariable" to hide / show a div.
AppRootModule:
export const appRootModule: IModule = module('datawalk', ['asyncFilter'])
.component('appRoot', new AppRootComponent())
.component('postModal', new PostModalComponent())
.service('componentsDataService', ComponentDataService);
PostModalComponent:
export class PostModalComponent {
public template: string;
public controller: Injectable<IControllerConstructor>;
public controllerAs: string;
constructor() {
this.controller = PostModalController;
this.controllerAs = 'postM';
this.template = PostModalTemplateHtml;
}
PostModalController
export class PostModalController implements IController {
public modalVisible: boolean;
/../
constructor(componentsDataService: ComponentDataService) {
/../
this.modalVisible = false;
/../
this.$cds.getModalData().subscribe((data: any) => {
if (data.showMod === true) {
this.modalOpen(data);
}
});
}
public $onInit = async (): Promise<void> => {};
public modalOpen(post: any): void {
console.error(this.modalVisible); // false
this.modalVisible = true;
console.error(this.modalVisible); // true
/../
}
And the template:
<div class="modal" ng-show="modalVisible">
<div class="modal-body">
/../
</div>
</div>
Anyone can tell me what I'm doing wrong?
console logs shows that the variable changes, but nothing happen, modal div is still hidden.
OK. So I've figured it out.
That's not a problem of code, but the problem is inside my IDE.
WebStorm has an issue - it does not recognize a controllerAs property in HTML templates of AngularJS in component architecture.
Also tslint works faulty.
I don't recommend to use that with AngularJS with component architecture.
Vs Code works perfect with that.
I have a simple app component with a search input and an observable resultItems: Observable<Array<string>>; powered by a search service that returns results to the UI via *ngFor. There is also a leaflet map that should render the locations of the results. The search service works well and I can render the location of one result in the map onclick. My question is: What is the recommended way to call the map service mapResults every time the search service returns new results or the observable changes? I can imagine how I could create a custom pipe that would iterate over the parks in the service results and call mapservice.mapResult but that seems odd since the pipe wouldn't return anything to the UI and I'm a little concerned with performance, understanding little about pure and impure pipes. I have also seen mention of a process by which you subscribe to changes of an observable, but I'm lost with the semantics and changes among API versions.
I apologize if this is a problem of poor design. I only have a few weeks of Angular learning and I admittedly haven't read the documentation thoroughly. Please point any and all issues you see.
simple Search Service
import { Injectable } from '#angular/core';
import { URLSearchParams, Jsonp } from '#angular/http';
#Injectable()
export default class ParkSearchService {
constructor(private jsonp: Jsonp) { }
search(parkSearchterm: string) {
var search = new URLSearchParams()
search.set('q', 'PARK_NAME:*' + parkSearchterm+'*');
search.set('wt', 'json');
search.set('json.wrf','JSONP_CALLBACK')
var test = this.jsonp
.get('http://parksearch/parks_full/select?', { search })
.map((response) => response.json()['response']['docs']);
return test
}
}
exert from app.component.html
<md-card *ngFor="let item of resultItems | async; let i = index"
class="search-result"
[ngClass]="{ 'selected-result': selectedIndex === i }">
<md-card-header class="clickable"
(click)="showBoundary(item)"
md-tooltip="Zoom to park">
<md-card-title>{{item.PARK_NAME}}</md-card-title>
</md-card-header>
<md-card-content style="height: 75px; overflow-y: auto">
<button md-button
color="primary"
md-tooltip="more info"
(click)="openDtl(item.PARK_HOME_PAGE_URL)">
<md-icon>info</md-icon>
<span>Details...</span>
</button>
<button md-button
color="primary"
md-tooltip="open park website"
(click)="openParkURL(item.PARK_HOME_PAGE_URL)">
<md-icon>web</md-icon>
<span>WebSite</span>
</button>
Amenties: {{ item.AMEN_LIST }}
</md-card-content>
</md-card>
app.component.ts (forgot to include)
export class AppComponent {
private selectedIndex: number;
public events: any[] = [];
//park search items
resultItems: Observable<Array<string>>;
parkSearchterm = new FormControl();
//setup resultitems
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm));
map service:
//Thanks for the help getting started https://github.com/haoliangyu
import { Injectable } from '#angular/core';
import { Map, GeoJSON } from 'leaflet';
#Injectable()
export default class MapService {
public map: Map;
private currentLayer: GeoJSON;
private resultsLayer: any;
private resultfeatureCollection: any;
constructor() {
}
initialize() {
if (this.map) {
return;
}
this.map = L.map('map', {
zoomControl: true,
zoom: 6,
minZoom: 3,
maxZoom: 19
});
L.tileLayer('http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap, Tiles courtesy of Humanitarian OpenStreetMap Team'
}).addTo(this.map);
L.control.scale().addTo(this.map);
//Add the results layer
this.resultsLayer = L.geoJSON(this.resultfeatureCollection, {
style: () => {
return {
color: '#ff00005',
fillColor: '#3F51B5'
};
}
}).addTo(this.map);
}
mapResults(park) {
//update the restults layer
let resultfeatureCollection: GeoJSON.FeatureCollection<any> = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: "Polygon",
coordinates: JSON.parse(park.BBOX[0])
},
properties: {
name: 'test'
}
}
]
};
this.resultsLayer.addData(resultfeatureCollection);
this.map.fitBounds(this.resultsLayer.getBounds());
}
}
You are very close to what you need: You already have an observable stream created for your results, called resultItems, that is right. Then on your template, when you use it through the async pipe what Angular does internally is subscribes to this stream to get its values.
So if you want to "also" map when resultItems yields is by subscribing to it by yourself as well. There's a catch though: by default every subscription duplicates the workload for the stream, meaning that every time the user makes a new search it would run the API call twice: 1 for the async subscription and another one for your .subscribe.
The way to resolve that is by using .publish(): What this allows is to share the result of an stream between many subscribers, so your code would look like this:
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm))
.publish();
// With publish() we are sharing the items from this stream to all of their subscribers
// We just need to tell it to do the "first subscription"
this.resultConnection = this.resultItems.connect();
// This "resultConnection" is the base subscription... We will have to dispose it in our ngOnDestroy method or we might get memory leaks
// Now we can tell the map service to update when we get a value:
this.resultItems.subscribe((park) => this.mapService.mapResults(park));
// (Not sure if the data types are correct, I assume you can map them)
// When `async` calls .subscribe() to do his job, we won't get duplicate API calls thanks to .publish()
}
To clarify, what connect() does is subscribe to the original stream, and start forwarding the values recieved to the subscribers of the published stream (this.resultItems). Now you are the owner of that subscription, so you are responsible of disposing it whenever you don't need more searches.
Subscribe on observer and call then search is changed. https://angular.io/docs/ts/latest/guide/router.html#!#reuse
After get result from server, put it to array. Angular will update the data in the template itself, this is the fastest way.
In Angular 1 we avoided calling functions from within template expressions in excess, e.g. ng-repeat="item in vm.getFilteredItems()" because property changes unrelated to the result of getFilteredItems would cause the function to recompute repeatedly and unnecessarily on digests, which commonly causes performance problems at scale. Instead we bound to objects, and compute based on events (e.g. ng-repeat="item in vm.filteredItems).
In Angular 2, the dirty checking process has been optimized but functions called in component templates will still be called when any properties at the component level change, regardless of whether the function is dependent on those properties. I expect this could lead to the same performance issues if used improperly.
Here is a simplified example of the differing approaches in Angular 2:
// function binding in template
#Component({
selector: 'func',
template: `
<input [(ngModel)]="searchTerm" placeholder="searchTerm" />
<div *ngFor="let name of filteredNames(searchTerm)">{{name}}</div>
`
})
export class FuncComponent {
#Input() names:string[];
filteredNames(searchTerm) {
if (!searchTerm) return this.names;
let filteredNames = [];
return this.names.filter((name) => {
return name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1;
});
}
}
-
// no function binding
#Component({
selector: 'nofunc',
template: `
<input [(ngModel)]="searchTerm" (ngModelChange)="search($event)" placeholder="searchTerm" />
<div *ngFor="let name of filteredNames">{{name}}</div>
`
})
export class NoFuncComponent implements OnInit {
#Input() names:string[];
searchTerm: string;
ngOnInit() {
this.search(this.searchTerm);
}
search() {
if (!this.searchTerm) {
this.filteredNames = this.names;
return;
}
this.filteredNames = this.names.filter((name) => {
return name.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
});
}
}
http://plnkr.co/edit/AAFknlJgso3D8F1w3QC1?p=preview
Is this still a concern in Angular 2? Which approach is preferred, and why? Thanks!
You could create a pipe. Pipes (if pure) are only called when depending values change.
#Pipe({
name: 'search',
// pure: 'false'
})
class SearchPipe {
transform(names, searchTerm) {
if (!this.searchTerm) {
this.filteredNames = names;
return;
}
return names.filter((name) => {
return name.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
});
}
}
#Component({
selector: 'func',
pipes: [SearchPipe],
template: `
<input [(ngModel)]="searchTerm" placeholder="searchTerm" />
<div *ngFor="let name of names | search:searchTerm">{{name}}</div>
`
})
export class FuncComponent {
#Input() names:string[];
}
If the pipe needs to recognize changes in names then pure needs to be disabled.
If names is replaced by a different instance instead of just members being added or removed, then pure works fine and the pipe is only executed when names or searchTerm changes.
Hint
In devMode (default) change detection runs twice and the pipe will be called twice as much as expected.