Template not binding to observable while using async pipe on initial load - javascript

I'm using the contentful js SDK to fetch data in a service. The SDK provides a method to retrieve entries from a promise and also parse those entries. Since I would like to use observables, I am returning the promise as an observable and then transforming from there.
In my home component, I am then calling the contentfulService OnInit and unwrapping the observable in the template using the async pipe.
My problem:
When the home component loads, the template is not there even though the service has fetched the data successfully. Now, if I interact with the DOM (click, hover) on the page, the template will instantly appear. Why is this not just loading asynchronously on page load? How can I fix this?
An example .gif showing the behavior.
contentful.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs/Rx';
import { Service } from '../models/service.model';
import * as contentful from 'contentful';
#Injectable()
export class ContentfulService {
client: any;
services: Service[];
service: Service;
constructor() {
this.client = contentful.createClient({
space: SPACE_ID,
accessToken: API_KEY
});
}
loadServiceEntries(): Observable<Service[]> {
let contentType = 'service';
let selectParams = 'fields';
return this.getEntriesByContentType(contentType, selectParams)
.take(1)
.map(entries => {
this.services = [];
let parsedEntries = this.parseEntries(entries);
parsedEntries.items.forEach(entry => {
this.service = entry.fields;
this.services.push(this.service);
});
this.sortAlpha(this.services, 'serviceTitle');
return this.services;
})
.publishReplay(1)
.refCount();
}
parseEntries(data) {
return this.client.parseEntries(data);
}
getEntriesByContentType(contentType, selectParam) {
const subject = new Subject();
this.client.getEntries({
'content_type': contentType,
'select': selectParam
})
.then(
data => {
subject.next(data);
subject.complete();
},
err => {
subject.error(err);
subject.complete();
}
);
return subject.asObservable();
}
sortAlpha(objArray: Array<any>, property: string) {
objArray.sort(function (a, b) {
let textA = a[property].toUpperCase();
let textB = b[property].toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
});
}
}
home.component.ts
import { Component, OnInit } from '#angular/core';
import { Observable } from 'rxjs/Rx';
import { ContentfulService } from '../shared/services/contentful.service';
import { Service } from '../shared/models/service.model';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
service: Service;
services: Service[];
services$: Observable<Service[]>;
constructor(
private contentfulService: ContentfulService,
) {
}
ngOnInit() {
this.services$ = this.contentfulService.loadServiceEntries();
this.services$.subscribe(
() => console.log('services loaded'),
console.error
);
};
}
home.component.html
...
<section class="bg-faded">
<div class="container">
<div class="row">
<div class="card-deck">
<div class="col-md-4 mb-4" *ngFor="let service of services$ | async">
<div class="card card-inverse text-center">
<img class="card-img-top img-fluid" [src]="service?.serviceImage?.fields?.file?.url | safeUrl">
<div class="card-block">
<h4 class="card-title">{{service?.serviceTitle}}</h4>
<ul class="list-group list-group-flush">
<li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Cras justo odio</li>
<li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Dapibus ac facilisis in</li>
<li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Vestibulum at eros</li>
</ul>
</div>
<div class="card-footer">
Learn More
</div>
</div>
</div>
</div>
</div>
</div>
</section>
...

It sounds like the Contentful promise is resolving outside of Angular's zone.
You can ensure the the observable's methods are run inside the zone by injecting NgZone into your service:
import { NgZone } from '#angular/core';
constructor(private zone: NgZone) {
this.client = contentful.createClient({
space: SPACE_ID,
accessToken: API_KEY
});
}
And by calling using the injected zone's run when calling the subject's methods:
getEntriesByContentType(contentType, selectParam) {
const subject = new Subject();
this.client.getEntries({
'content_type': contentType,
'select': selectParam
})
.then(
data => {
this.zone.run(() => {
subject.next(data);
subject.complete();
});
},
err => {
this.zone.run(() => {
subject.error(err);
subject.complete();
});
}
);
return subject.asObservable();
}

Related

how to show a Blog detail link in Angular

I want to show the Detail of a Blog in a different link in Angular. I already have a Blog file (blog.component.ts) and an Angular service where I can get all the blogs data from an API backend made with Strapi. There is one button in every single blog, which allows you to view the detail or complete Blog in a different link calling the unique ID, that is named 'pagina.component.ts'.
For that purpose, I think I must call the ID of every single Blog.
Here is my blog.component.html, where I already have the list of my blogs:
<section class="articles">
<article class="blue-article" *ngFor="let data of datas; index as i">
<div class="articles-header">
<time>{{ data.fecha }}</time>
<span class="articles-header-tag-blue">{{ data.relevante }}</span>
<span class="articles-header-category">
{{ data.category.name }}
</span>
</div>
<div class="articles-content">
<h1><a title="">{{ data.title }}</a></h1>
<!--<img *ngIf="!data.image" class="foto" [src]="data.image.name" alt="foto">-->
<div *ngIf="data.image">
<img
src="http://localhost:1337{{ data.image.url }}"
alt="foto"
width="100%"
/>
</div>
<p>
{{ data.content }}
</p>
<h3>{{ data.description }}</h3>
</div>
<div class="articles-footer">
<ul class="articles-footer-info">
<li><i class="pe-7s-comment"></i> 7 Respuestas
</li>
<li><i class="pe-7s-like"></i> 1221</li>
</ul>
<a [routerLink]="['./pagina',i]" class="btn">Leer más</a>
</div>
</article>
</section>
Here is my blog.component.ts file
import { Component, OnInit } from '#angular/core';
import { Meta, Title } from '#angular/platform-browser';
import { StrapiService } from '../../../services/strapi.service';
import { Router } from '#angular/router';
#Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.scss']
})
export class BlogComponent implements OnInit {
datas:any=[];
errores:string="";
constructor(
public strapiserv:StrapiService,
private router: Router
) { }
ngOnInit(): void {
this.title.setTitle('Blog');
this.strapiserv.getData().subscribe(res=>{
this.datas= res as string[];
}, error =>{
console.log(error);
if(error.status == 0){
this.errores="Código del error: "+error.status+" \n Ha ocurrido un error del lado del cliente o un error de red.";
}else{
this.errores="Código del error: "+error.status+"\n\n"+error.statusText;
}
})
}
}
Here is my Angular service named 'strapi.service.ts'
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class StrapiService {
REST_API: string ='http://localhost:1337/articles';
//https://strapi-dot-gmal-api-313723.uc.r.appspot.com/
httpHeaders = new HttpHeaders().set('Content-Type', 'application/json');
constructor(private httpClient: HttpClient) { }
getData():Observable<any>{
console.log();
let API=this.REST_API;
return this.httpClient.get(API,{headers:this.httpHeaders}) .pipe(
map((data:any) => {
return data;
}), catchError( error => {
return throwError(error);
})
)
}
/*getItem( idx:number ){
return this.data[idx];
}*/
}
And Here is my pagina.component.ts file where I want to show the complete detailed Blog
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Router } from '#angular/router';
import { StrapiService } from '../../../../services/strapi.service';
#Component({
selector: 'app-pagina',
templateUrl: './pagina.component.html',
styleUrls: ['./pagina.component.scss']
})
export class PaginaComponent implements OnInit {
data:any = {};
constructor( private activatedRoute: ActivatedRoute,
private router: Router,
public strapiserv:StrapiService
){
this.activatedRoute.params.subscribe( params => {
this.data = this.strapiserv.getData( params['id'] );
});
}
ngOnInit(): void {
}
}
My routes are:
const routes: Routes = [
{ path: 'blog', component: BlogComponent },
{ path: 'pagina/:id', component: PaginaComponent },
];
There are a lot of issues with what you have done:
The 'i' is the index of the loop, not the 'id' you want to use. I guess the 'id' is a property of data, so you should point to data.id
You don't have to use relative path when using [routerLink] (with brackets)
so replace this:
[routerLink]="['./pagina',i]"
with this:
[routerLink]="['/pagina', data.id]" // remove the '.'
Finally in your blog.component.ts file, your're receiving array of objects so you don't have to cast it as an array of string, this.datas= res; is enough

Why array only brings one object in Angular project?

I'm working with the League of Legends API, more specific with the champion json they bring.
I have this service made with Angular:
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders} from '#angular/common/http';
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
}
#Injectable({
providedIn: 'root'
})
export class ChampionsService {
constructor(private http: HttpClient) { }
getChampions(){
return this.http.get('http://ddragon.leagueoflegends.com/cdn/10.23.1/data/es_ES/champion.json');
}
}
This is my .ts file:
import { Component, OnInit } from '#angular/core';
import {ChampionsService } from '../../services/champions.service';
#Component({
selector: 'app-champions',
templateUrl: './champions.component.html',
styleUrls: ['./champions.component.css']
})
export class ChampionsComponent implements OnInit {
public champions;
public arrayChampions;
constructor(private championsService:ChampionsService) { }
ngOnInit(): void {
this.getAllChampions();
}
getAllChampions(){
this.championsService.getChampions().subscribe(
data => { this.champions = data,
this.arrayChampions = Object.entries(this.champions.data).map(([k,v]) => ({ [k]:v })),
this.ArrayIterator();
},
err => {console.error(err)},
() => console.log("Champions cargados")
);
}
ArrayIterator() {
let IteratableArray = Array();
for (let item of Object.keys(this.arrayChampions[0])) {
var eventItem = Object.values(this.arrayChampions[0]);
IteratableArray.push(eventItem);
}
this.arrayChampions = IteratableArray[0];
}
}
And this is the html:
<p>champions works!</p>
{{arrayChampions | json}}
<!-- Cards -->
<div *ngFor="let arrayChampion of arrayChampions" class="card mb-3">
<div class="card-header">
</div>
<div class="card-body">
<blockquote class="blockquote mb-0">
<a class="text-decoration-none">{{arrayChampion.id}}</a>
</blockquote>
</div>
<div class="card-footer">
</div>
</div>
As you can see, the var "arrayChampions" only brings the first champion (Atrox) when it should bring all the champions as I understand (I'm new at javascript and Angular).
As per your exmaple I have created and stackblitz here, And it made the whole arrayChampions Iterate over its values.
Please find the working stacblitz here
Sample HTML :
<hello name="{{ name }}"></hello>
<!-- {{this.products |json}} -->
<ul>
<li *ngFor="let champ of products | keyvalue">
<label style="font-size: 20px;font-weight: bold;color: red;">
{{champ.key}}
</label>
<ul *ngFor="let item of champ.value | keyvalue">
<li>
{{item.key}} : {{item.value}}
<ul *ngFor="let sub of item.value | keyvalue">
<li>
{{sub.key}} : {{sub.value}}
</li>
</ul>
</li>
</ul>
</li>
</ul>
Sample component.ts :
import { Component } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { map, catchError, tap } from "rxjs/operators";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
apiURL: string =
"https://ddragon.leagueoflegends.com/cdn/10.23.1/data/es_ES/champion.json";
name = "Angular";
products = [];
constructor(private httpClient: HttpClient) {}
ngOnInit() {
this.getChamp();
}
getChamp() {
this.httpClient.get(this.apiURL).subscribe((data: any) => {
this.products = data.data;
Object.keys(data.data).map((key: any, obj: any) => obj[key]);
});
}
}

Integrate Ng-Survey multiple time inside the same component

Im using ng-surveys template inside my angular application
https://ng-surveys.firebaseapp.com/
I put the template selector "" inside *ngFor to make multiple surveys in the same page.
It works but the browser considers all the surveys as the same survey : whene I change something in one survey it changes in the other surveys.
I tried to integrate the template dynamicly using Angular ComponentResolver but I faced many errors and I'm not sure this way can fix my problem.
create-assignment.component.html:
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<i class="fa fa-file-text-o fa-lg"></i> Questionnaires
</div>
<div class="card-body">
<tabset>
<tab *ngFor="let survey of surveys; let i = index">
<ng-template tabHeading><img [src]="survey.category_icon"/> {{survey.category_name}}</ng-template>
<!-- <app-create-survey></app-create-survey> -->
<!-- <ng-template #dynamic></ng-template> -->
<ngs-builder-viewer [options]="options"></ngs-builder-viewer>
</tab>
</tabset>
</div>
</div>
</div>
</div>
create-assignment.component.ts:
import { Component, OnInit, ViewChild, ViewContainerRef, Inject, AfterViewInit, AfterViewChecked } from '#angular/core';
import { LocalStoreService } from '../../../shared/services/local-store.service';
import { ApplicationService } from '../../../shared/services/application.service';
import { NgbDateParserFormatterService } from '../../../shared/services/ngb-date-parser-formatter.service';
import { Router } from '#angular/router';
import { ToastrService } from 'ngx-toastr';
import { IBuilderOptions, NgSurveyState, IElementAndOptionAnswers } from '../../../shared/ng-surveys/models';
import { Observable } from 'rxjs';
import { Survey } from '../../../shared/models/survey';
import { LoaderService } from '../../../shared/services/loader.service';
#Component({
selector: 'app-create-assignment',
templateUrl: './create-assignment.component.html',
styleUrls: ['./create-assignment.component.scss']
})
export class CreateAssignmentComponent implements OnInit, AfterViewChecked {
options: IBuilderOptions;
assignment: any = {};
pending: Boolean = false;
user: any;
shops: any = [];
survey_categories: any = [];
surveys: any = [];
errors: any = {};
service: any
#ViewChild('dynamic', {
read: ViewContainerRef , static: false
}) viewContainerRef: ViewContainerRef
constructor(
#Inject(LoaderService) service,
private ls: LocalStoreService,
public router: Router,
public toastr: ToastrService,
private dateService: NgbDateParserFormatterService,
private appServ: ApplicationService
) {
this.service = service
}
ngAfterViewChecked() {
// this.service.setRootViewContainerRef(this.viewContainerRef)
// this.service.addDynamicComponent()
}
ngOnInit() {
this.options = {
importSurvey: {
callback: this.importSurvey.bind(this),
},
surveyButtons: [{
title: 'Sauvegarder questionnaire',
icon: 'i-Data-Save',
text: 'Sauvegarder',
callback: this.saveSurvey.bind(this),
}],
importElement: {
callback: this.importElement.bind(this),
},
elementButtons: [{
title: 'Sauvegarder question',
icon: 'i-Data-Save',
text: 'Sauvegarder',
callback: this.saveElement.bind(this),
}]
};
this.appServ.getSurveyCategories().subscribe((response: any) => {
this.survey_categories = response.data;
this.survey_categories.forEach(el => {
this.surveys.push(new Survey(el.id, el.name, el.icon, this.options));
});
});
this.user = this.ls.getItem('user');
this.appServ.getBusinessShops(this.user).subscribe((response: any) => {
this.shops = response.data;
});
}
importSurvey(): Observable<NgSurveyState> {
// Mocking get request
return this.getSurvey();
}
importElement(): Observable<IElementAndOptionAnswers> {
// Mocking get request
return this.getElement();
}
getSurvey(): Observable<NgSurveyState> {
return
}
getElement(): Observable<IElementAndOptionAnswers> {
return
}
saveSurvey(ngSurveyState: NgSurveyState): void {
console.log(ngSurveyState);
}
saveElement(element: IElementAndOptionAnswers): void {
// Add post request to save survey data to the DB
console.log('element: ', element);
}
toObject = (map = new Map) =>
Array.from
( map.entries()
, ([ k, v ]) =>
v instanceof Map
? { key: k, value: this.toObject (v) }
: { key: k, value: v }
)
}
I want to make each survey not similar to other surveys
If you look into source code of that library:
In this file projects/ng-surveys/src/lib/ng-surveys.module.ts
These reducers(services) contain survey object:
NgSurveyStore, SurveyReducer, PagesReducer, ElementsReducer, OptionAnswersReducer, BuilderOptionsReducer provided under module level injectors if we can provide these in component level injectors data wouldnt be shared.
Since they are provided in module and when you import in your own module, they are sharing single instance of these reducers which store state survey object.

Angular 6 pagination with ng-bootstrap of fetched data from Api

Can someone help me fix my code for pagination?
There is a service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable ()
export class MovieRequestService {
private moviesList = `https://yts.am/api/v2/list_movies.json`;
private movieDetails = `https://yts.am/api/v2/movie_details.json`;
constructor(private myHttp: HttpClient ) {
}
getListOfMovies(page: number, collectionSize: number): any {
return this.myHttp.get(`${this.moviesList}?limit=${collectionSize}&page=${page}`);
}
getMovieDetails(id: number): any {
return this.myHttp.get(`${this.movieDetails}?movie_id=${id}&with_images=true&with_cast=true`);
}
}
Second file is component:
import { Component, OnInit } from '#angular/core';
import { MovieRequestService } from '../../shared/services/movie-request.service';
import { HttpClient} from '#angular/common/http';
import { ContentChild } from '#angular/core';
import { NgbPagination } from '#ng-bootstrap/ng-bootstrap';
#Component({
selector: 'app-movie-list',
templateUrl: './movie-list.component.html',
styleUrls: ['./movie-list.component.css']
})
export class MovieListComponent implements OnInit {
#ContentChild(NgbPagination) pagination: NgbPagination;
page = 1;
collectionSize = 40;
movies: any[] = new Array;
constructor(private movieService: MovieRequestService, private http: HttpClient) {
this.getPageFromService();
}
getPageFromService() {
this.movieService.getListOfMovies(this.page, this.collectionSize).subscribe(response => this.movies = response.data.movies);
}
ngOnInit() {
}
}
And last one is html of the component:
<div class="row" >
<div class="col-sm-3 flex-md-wrap" *ngFor="let movie of movies">
<div class="card mb-3">
<a >
<figure class="figure">
<img src="{{movie.medium_cover_image}}" alt="{{movie.title}}" class="figure-img img-fluid rounded mx-auto d-block" width="253">
<figcaption class="figure-caption">
</figcaption>
</figure>
</a>
<a routerLink="movie/{{movie.id}}"><div>{{ movie.title | json}}
</div></a>
<div class="mb-4">{{ movie.year | json}}</div>
</div>
</div>
<ngb-pagination
class="d-flex justify-content-center"
(pageChange)="getPageFromService()"
[collectionSize]="collectionSize"
[(page)]="page"
[boundaryLinks]="true">
</ngb-pagination>
</div>
Result:
As you can see, there are only 4 pages of paginations but there should be more than 200 (db has 8973 movies with limit of 40 per page).
Example of json from api:
Your collectionSize is 40. By default page size is 10. Thus you see 4 pages.
You need to change your collectionSize = your movie count and pageSize of 40.
Edit:
this.movieService
.getListOfMovies(this.page, this.pageSize)
.subscribe(response => {
this.movies = response.data.movies;
this.collectionSize = response.data.movie_count;
});
Edit 2:
Seems like page doesn't get updated due to the change detection, you need to pass the emitted event from pageChange, you can see that if you log your this.page it is always returning the previous value.
On your HTML:
<ngb-pagination
(pageChange)="getPageFromService($event)"
[pageSize]="pageSize"
[(page)]="page"
[maxSize]="5"
[rotate]="true"
[collectionSize]="collectionSize"
[boundaryLinks]="true"
></ngb-pagination>
On .ts
getPageFromService(page) {
this.movieService.getListOfMovies(page, this.pageSize).subscribe(response => {
this.movies = response.data.movies;
this.collectionSize = response.data.movie_count;
});
}

Render results in template on completed request from server

I am building my first Angular app and need some help. The component I'm working on is an image search box. The user enters search query, request is sent to API, API responds with JSON data. Why is my *ngFor loop not working? The iterable is updated when the server sends response.
image-search.component.ts
import { Component, OnInit } from '#angular/core';
import { ImageSearchService } from './image-search.service';
import { Image } from '../shared/image';
#Component({
selector: 'vb-image-search',
templateUrl: './image-search.component.html',
styleUrls: ['./image-search.component.css'],
providers: [ImageSearchService]
})
export class ImageSearchComponent implements OnInit {
images: Image[] = [];
constructor(private ImageSearchService: ImageSearchService) { }
ngOnInit() {
}
getImages(query: string) {
this.ImageSearchService.getImages(query)
.subscribe(function(images) {
this.images = images;
});
}
onClick(query:string) {
this.getImages(query);
}
}
image-search.service.ts
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Http, Response } from '#angular/http';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Image } from '../shared/image';
#Injectable()
export class ImageSearchService {
constructor(private http: Http) {}
getImages(query: string): Observable<any[]> {
return this.http.get(`http://localhost:3000/api/search/${query}`)
.map(this.extractData)
}
private extractData(res: Response) {
let body = res.json();
return body.data.map(e => new Image(e.farmID, e.serverID, e.imageID, e.secret)) || {};
}
}
image.ts
export class Image {
constructor(public farmID: string, public serverID: string, public imageID: string, public secret: string) {
this.farmID = farmID;
this.serverID = serverID;
this.imageID = imageID;
this.secret = secret;
}
}
image-search.component.html
<div class="col-lg-6 col-md-6">
<div class="input-group">
<input type="text" [(ngModel)]="query" class="form-control" placeholder="Search for images..." />
<span class="input-group-btn">
<button (click)="onClick(query)" class="btn btn-default" type="button">Go!</button>
</span>
</div>
<h2>Images</h2>
<div *ngFor="let image of images">
{{image.imageID}}
</div>
</div>
The reason is very simple. In typescript the function call back loses the current scope if you use function(){} so instead you have to used => {} to retain the current scope. So please modify your current getImages method as mentioned below:
getImages(query: string) {
this.ImageSearchService.getImages(query)
.subscribe(images => {
this.images = images;
});
}

Categories

Resources