ngFor not update view unless click on view Angular - arrays

I tried detectChanges(), marforCheck() as well, reassign but nothing changes. They just change after I click on view again.
Scenario:
I get data from store, I tracked data change successfully in subscribe function.
this.allInfo$.subscribe(data => {
this.allInfos = data;
});
<div (click)="addInfo(social)" *ngFor="let info of allInfos"
[ngClass]="{'active': info.connected}">
<span>{{info.name}} {{info.connected}}</span>
</div>
When I click addInfo, app will open thirdparty dialog like firebase login, if success will return new data and update to allInfo$ I tracked. But the view does not changes? Is that angular bug? View change if I click somewhere on screen
My angular version 7
UPDATED: My problem was resolved by using _ngZone.run()

You kept the click event on the , so when you are clicking on that div it is updating.
<div *ngFor="let info of allInfos"
[ngClass]="{'active': info.connected}">
<span (click)="addInfo(social)">{{info.name}} {{info.connected}}</span>
</div>
in this code the view will update when you click on the span

trackBy
<div (click)="addInfo(social)" *ngFor="let info of allInfos;trackBy:trackByFunction"
[ngClass]="{'active': info.connected}">
<span>{{info.name}} {{info.connected}}</span>
</div>
.ts
trackByFunction(index, info){
if(!info)return null;
return info.id; //or return index;
}
Link:-https://medium.com/better-programming/improving-angular-ngfor-performance-through-trackby-ae4cf943b878

a subscription not put a "listener" in the dbs, so any change in the dbs is reflex. Each change in the dbs, you need or get the list again or add manually to your array allInfos.
You say "When I click addInfo, app will open thirdparty dialog like firebase login, if success will return new data and update to allInfo$"
I have no idea about your addInfo, so I supouse your addInfo is like
addInfo(data)
{
this.service.addData(data).subscribe(res=>{
console.log(res) //generally success
})
}
You can so some like
addInfo(data)
{
this.service.addData(data).subscribe(res=>{
if (sucess)
this.allInfos.push(data)
})
}
or
addInfo(data)
{
this.service.addData(data).pipe(
switchMap(res=>{
return res.success?this.allInfo$:of(null)
})
).subscribe(res=>{
if (res)
this.allInfos=res
})
}
It's possible that the "thirdparty dialog make a next to your $allInfo", (than all of this it's unnecesary) but I don't know. Imagine that you has a typical CRUD service like
#Injectable()
export class ProductService {
endpoint: string = '[YOUR_DB_SERVER_IP]';
constructor(
private http: HttpClient
) {}
// CREATE
createProduct(productToCreate: IProduct): Observable<IProduct[]> {
return this.http.post(`${this.endpoint}/products`, productToCreate);
}
// READ
readProducts(): Observable<IProduct[]> {
return this.http.get(`${this.endpoint}/products`);
}
// UPDATE
updateProduct(objToUpdate: IProduct): Observable<IProduct[]> {
return this.http.patch(`${this.endpoint}/products/${objToUpdate.id}`, objToUpdate};
}
// DELETE
deleteProduct(): Observable<IProduct[]> {
return this.http.delete(`${this.endpoint}/products/${objToDelete.id}`);
}
}
We are changing so, each change emit an event
private dbsChangeSubject = new Subject<any>();
dbsChange = this.dbsChangeSubject .asObservable();
// CREATE
createProduct(productToCreate: IProduct): Observable<IProduct[]> {
return this.http.post(`${this.endpoint}/products`, productToCreate).pipe(
tap(result=>{
if (result.success)
this.dbsChangeSubject.next(true)
}))
);
}
// UPDATE
updateProduct(objToUpdate: IProduct): Observable<IProduct[]> {
return this.http.patch(`${this.endpoint}/products/${objToUpdate.id}`, objToUpdate.pipe(
tap(result=>{
if (result.success)
this.dbsChangeSubject.next(true)
}))
);;
}
// DELETE
deleteProduct(): Observable<IProduct[]> {
return this.http.delete(`${this.endpoint}/products/${objToDelete.id}`).pipe(
tap(result=>{
if (result.success)
this.dbsChangeSubject.next(true)
}))
);
}
}
Know we can subscribe to
this.service.dbsChange().pipe(
startWith(true),
switchMap(()=>{
this.service.readProducts()
})).subscribe(res=>{
this.products=res
})
Here we subscribe to "dbsChange", not to readProducts(), so we are listen to all the this.dbsChangeSubject.next that we has

As dlam said in his edited question, using NgZone in the new window solves the problem.
Example:
import { NgZone} from '#angular/core';
export class NewWindowComponent implements OnInit{
constructor(private ngZone: NgZone) { }
ngOnInit() {
this.someService.getChangedData.subscribe(data => {
this.ngZone.run(() => {
this.someData= data;
});
});
}
}

Related

Dynamic array with angular

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

Angular problem of How can I show only one record in an array object?

I'm facing a problem about the when I get one record from array like this
data.service.ts
getOneBookDetail(isbn:any) {
const headers = new HttpHeaders().set("Content-Type", "application/json");
// console.log("=============" + isbn )
console.log(headers)
return this.http.get('http://localhost:10888/bookdetail/?isbn='+ isbn).subscribe(
(val) => { // Get has no error and response has body
console.log("Get successful value returned in body", val);
},
response => {
console.log("Get call in error", response);
},
() => { // Get has no error, response has no body
console.log("The Get observable is now completed.");
});
}
home.component.ts
getBookDetail(book) {
this.data.getOneBookDetail(book.isbn) //isbn of book
}
and I can click the title of book
<a routerLink="/bookdetail/{{book.isbn}}" (click)="getBookDetail(book)"><h3>{{ book.title }}</h3></a>
and I can get a object I saw it in console
Get successful value returned in body [{…}]
0: {_id: "5fc91e5aa700213eb8c52de0", title: "A Promised Land"
[{…}] is 0: {_id: "5fc91e5aa700213eb8c52de0", title: "A Promised Land"
....
and now I want to get this object to a page call bookdetail to show only this book details but now still show all book
the below is the bookdetail component
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-bookdetail',
templateUrl: './bookdetail.component.html',
styleUrls: ['./bookdetail.component.scss']
})
export class BookDetailComponent implements OnInit {
h1Style: boolean = false;
books: Object;
constructor(private data: DataService) {}
ngOnInit() {
this.data.getBooks().subscribe(data=> {
console.log({data}) //show data
this.books = data
//console.log(this.books);
})
}
}
in bookdetail html
<h1>Book-detail</h1>
<div *ngIf="books" class="bookdetail-block">
<div *ngFor="let bookdetail of books" class="bookdetail">
<h1>{{bookdetail.title}}</h1>
<p><img [src]="bookdetail.image" ></p>
<p>{{bookdetail.author}}</p>
<p>{{bookdetail.price}}</p>
<p>{{bookdetail.isbn}}</p>
<p>{{bookdetail.description}}</p>
</div>
</div>
How can I only show I have choose?
I think the issue is in bookdetail ngOnInit()??
Zico, the idea generally is that you subscribe to ActiveRouter.params IN your "detail-component", see the docs: Well you use a switchMap to, after get the parameter, make the dataService.getOneBookDetail(id). In subscribe you equal the response to a variable and ony show the variable
book:any
constructor(private activatedRoute:ActivatedRoute,private dataService:DataService){}
ngOnInit() {
this.route.paramMap.pipe(
switchMap(params => {
const ibs=params.get('isbn');
return this.dataService.getOneBookDetail(ibs);
}).subscribe(res=>{
book=res;
})
);
}
Other idea is pass data between routes like show Netanel Basal

Display values from API In Angular 6 page

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>

Can't call forEach on model hash in controller

I have a route called tickets which has it's model setup like so
model() {
return Ember.RSVP.hash({
event: null,
tickets: null
});
},
actions: {
didTransition(){
if(!this.controller.get('model.length')){
new Ember.RSVP.hash({
event: this.modelFor('events.event'),
tickets: this.store.query('ticket',{user_id:this.get('user.user.user_id'),event_code:this.modelFor('events.event').get('event_code')})
}).then((hash)=>{
if(!hash.tickets.get('length')){
this.controller.set('noTickets',true);
}
this.controller.set('model',hash);
});
}
}
}
The template manages to loop over these model.tickets just fine in a {{#each}} block
In my controller I'm attempting to setup a groupBy computed, but in my computed is where I get the error
ticketsByPurchase: Ember.computed('model.tickets.[].ticket_purchase_code',function(){
let tickets = this.get('model.tickets');
tickets.forEach(function(ticket){
console.log(ticket);
});
})
Try guarding against the model.tickets being iterated over, with something like this in the computed property:
if(!tickets){
return Ember.A()
}else{
//your forEach code here
}
or this in your route:
}).then((hash)=>{
if(!hash.tickets.get('length')){
this.controller.set('noTickets',true);
hash.tickets = Ember.Array([])
this.controller.set('model',hash);
}else{
this.controller.set('model',hash);
}
});
this.controller.get('model.length') is always null because model is a hash, not an array. Also didTransition is not an action, it's a regular function, so it probably will not fire if defined in the actions hash.
I would recommend removing the call to didTransition and do all the logic in your model hook like so:
model() {
let event = this.modelFor('events.event');
let event_code = event.get('event_code');
// this probably does not work, you have to get the user
// from another route or a service.
// this might work: this.controllerFor('user').get('user.user_id');
let user_id = this.get('user.user.user_id');
return Ember.RSVP.hash({
event,
tickets: this.store.query('ticket', { user_id, event_code })
});
},
setupController(controller, model) {
// super sets controller model to be route model.
// ie. controller.set('model', model);
this._super(...arguments);
if (!model.get('tickets.length')) {
controller.set('noTickets', true);
}
}

Angular2 component view updated continuously

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...

Resources