i am fetching some data via a angular service and takes about 2-3s to appear . so the HTML elements corresponding to that service also takes time to load . is there any way to show loading animation for that particular elements?
You need to track a loading state and have the template react accordingly.
If you're not using something like #ngrx/store for example, have a variable in your component.
isLoading = false
When fetching data, change that variable...
getSomeData(){
this.isLoading = true;
this.service.getSomeData().subscribe((data) => {
//... Do stuff with data
this.isLoading = false;
})
}
...then in your template...
<ng-container *ngIf="!isLoading; else loader">
<!-- Visible if not loading -->
</ng-container>
<ng-template #loader>
<!-- Loader element -->
</ng-templtate>
Be aware this is a VERY simple example of this and is often better handle via some sort of state management.
you can use the finalize operator from RxJs
https://www.learnrxjs.io/learn-rxjs/operators/utility/finalize
getData() {
this.showLoader = true;
this.service.getYourObs().pipe(finalize(() => this.showLoader = false)).subscribe(...);
}
All you need to know is whether the request is finished or not and set loading flag according to it.
AppComponent HTML
<ng-container>
<loading-el *ngIf = "loading"></loading-el>
<main-components *ngIf = "!loading"></main-components>
</ng-container>
Appcomponent.ts
loading = false;
getRecord() {
this.loading = true;
this.http.get(rec => {
this.loading = false;
...operation...
})
}
This is a pretty standard requirement and you have some options.
The simplest is to create a boolean property on your component and set it to true in the ngOnit hook. Here is a link that steps through it.
How to show spinner in angular 6
...as an alternative you could write an angular resolver to get your data prior to your component loading.
https://dzone.com/articles/understanding-angular-route-resolvers-by-example#:~:text=A%20Resolver%20is%20a%20class,may%20also%20like%3A%20Angular%20Resolvers.
Both require a loader graphic of some kind, the resolver allowing you to handle loading at an application level, which stops duplication in your components.
You actually don't need to manage a flag at the component level.
Use observables.
import { Component, OnInit } from '#angular/core';
import { from, Observable } from 'rxjs';
class Item {
constructor(public name = 'New Item') {}
}
#Component({
selector: 'my-app',
template: `
<button (click)="loadItems()">Load</button>
<hr>
<ng-container *ngIf="items$ | async as items; else loading">
<div *ngFor="let item of items; let index = index">
{{index+1}}. {{ item.name }}
</div>
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
`,
})
export class AppComponent implements OnInit {
items$: Observable<Item[]>;
ngOnInit() {
this.loadItems();
}
private loadItems() {
// Simulate HTTP request
const promise: Promise<Item[]> = new Promise((resolve) => {
setTimeout(() => {
resolve([new Item(), new Item(), new Item()]);
}, 750);
});
// Where the magic happens
// Reassign the observable, which will trigger the else
// block in the template and display the loader.
// Once it completes, it will display the list as normal.
this.items$ = from(promise);
}
}
Related
I have component A which is navbar and component B which is list view
Navbar has dropdown where all the users are listed, onInit method of component B all the data is loaded in the Component B Table that selected user is in local storage, now i want when the user changes the users from dropdown then that users data should automatically be refreshed in the Component B selected users are saved in localstorage. Is there anyway i can call method of second component from first component.
Navbar Component
<select class="form-control"(change)="selectUsers($event)" [(ngModel)]="UserId">
<option *ngFor="let opt of userlist" value={{opt.UserId}}>{{opt?.userName}}
</option>
</select>
selectUsers(event) {
this.Service.SelectedUserId = event.target.value;
}
In Service File
set SelectedUserId(value: string) {
if (value) localStorage.setItem('ActiveUserId', value);
}
Component B:
fetch() {
this.Userslist = [];
this.param.userId = localStorage.getItem('ActiveUserId');
this.service.getUsers(this.param).subscribe((data => {
this.Userslist = data.response;
}
}
In View:
<tbody>
<tr *ngFor="let data of Userslist | paginate:{itemsPerPage: 10, currentPage:p} ; let i=index"
..
..
</tbody>
Any solution to resolve this issue, Thanks
setup a common service which is shared by the two components. Lets assume its called testService.
so service will be like.
export class testService {
testEmitter: Subject<void> = new Subject<void>()
testObserver$: Observable<void>;
constructor () {
this.testObserver$ = this.testEmitter.asObservable();
}
}
Now the power of this pattern is that we can listen for next of the subject from any component in the application and perform action!
so in the navbar we start the next which will trigger action on all the observables!
selectUsers(event) {
this.Service.SelectedUserId = event.target.value;
this.testService.next();
}
So we subscribe on the component where the refresh should happen, and then reload the grid.
child component.
export class ChildComponent implements OnInit, OnDestroy {
subscription: Subscription = new Subscription();
ngOnInit() {
this.subscription.add(
this.testService.testObserver$.subscribe(() => {
this.fetch();
})
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
You can also emit objects like {eventType: 'test', data: {}} inside the next and then add multiple if conditions to validate the eventType and perform multiple actions on the same subject!
I have a rich text element in Contentful that has an embedded entry within it. I am attempting to have that embedded entry render inside a custom Angular component. However, when I pass in options to the Contentful documentToHtmlString function, it displays the tag, but does not render anything or even trigger the console.log() function I have inside the custom component typescript file.
TYPESCRIPT for the rendering
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => `<embed-element [node]="${node}" [content]="${next(node.content)}"></embed-element>`
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
HTML for the rendering
<span [innerHTML]="convertToHTML(article.fields.content)"></span>
I have loaded the custom element <embed-element></embed-element> in the providers section of the app.module.ts file as well.
import { EmbedElementComponent } from './shared/components/embed-element/embed-element.component';
#NgModule({
declarations: [
...
EmbedElementComponent
],
providers: [
...
EmbedElementComponent
]
})
Then inside typescript file for the custom element, I just simply have a console.log in the onInit function for testing purposes. I am not seeing this occur in the console.
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'embed-element',
templateUrl: './embed-element.component.html',
styleUrls: ['./embed-element.component.css']
})
export class EmbedElementComponent implements OnInit {
#Input() node: any;
#Input() content: any;
constructor() { }
ngOnInit(): void {
console.log(this.node);
}
}
And in the HTML for the custom element, I removed the <p> tag just in case it was stripping it out as "unsafe" and replaced it with the following:
EMBEDDED ELEMENT WORKS!!!!!
Finally, I see nothing appear on screen for this custom element once everything is rendered. Inside the inspect element however, this is what I get.
<embed-element [node]="[object Object]" [content]=""></embed-element>
How do I manage to make the custom element actually get called in this aspect? And at least receive the console log message I am requesting inside the custom element?
Not sure if you still needed another solution, but I came across ngx-dynamic-hooks https://github.com/MTobisch/ngx-dynamic-hooks - and was able to reuse my custom components within the inner html.
const cmpt_data = {
title: 'This is a test'
};
const headerFooterRichTextOption: Partial<Options> = {
renderNode: {
["embedded-entry-inline"]: (node, next) => `${this.embeddedContentfulEntry(node.nodeType, node.data)}`,
["paragraph"]: (node, next) => `<span>${next(node.content)}</span>`,
}
};
const d = documentToHtmlString(tt.value, headerFooterRichTextOption);
const hSanit = this.sanitizer.bypassSecurityTrustHtml(d);
embeddedContentfulEntry(nodeType: any, data: any) {
return "<custom-component [title]='context.title'></custom-component>";
}
<ngx-dynamic-hooks [content]="hSanit" [context]="cpmt_data"></ngx-dynamic-hooks>
I believe that using a custom component for this type of situation was not working because it was rendering outside of Angulars scope or after initial components initiation.
I resolved this issue by essentially just removing the custom element all together and creating a function that renders the embedded element as I wanted.
// Notice the new function call, renderCustomElement()
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => this.renderCustomElement(node)
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
And finally that function that simply creates some basic html with Bootstrap styling and returns that back to be rendered.
renderCustomElement(node:any) {
let link = `/support-center/article/${node.data.target.fields.category[0].sys.id}/${node.data.target.sys.id}`;
return `<style>.card:hover{box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;}</style><a class="card clickable mb-2 text-decoration-none text-dark" href="${link}" target="_blank"><div class="card-header">Article</div><div class="card-body"><h4 class="fw-light">${node.data.target.fields.title}</h4><p class="fw-light pb-0 mb-0">${node.data.target.fields.preview}</p></div></a>`
}
Im getting items from a cart service and putting them into my shopping cart component by making them public shoppingCartItems$ Observable<ISticker[]> = of ([]) and then subscribing that public shoppingCartItems: ISticker[] = [];
but when I place them into my html in an ngfor loop it tells me the properties are undefined
import { Component, OnInit } from '#angular/core';
import {CartService} from '../cart.service'
import { of } from 'rxjs';
import {Observable} from 'rxjs'
import { ISticker } from '../classes/isticker';
#Component({
selector: 'app-shopping-cart-component',
templateUrl: './shopping-cart-component.component.html',
styleUrls: ['./shopping-cart-component.component.css']
})
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]> = of([]);
public shoppingCartItems: ISticker[] = [];
constructor(private cartService: CartService) {
this.shoppingCartItems$ = this
.cartService
.getItems();
this.shoppingCartItems$.subscribe(_ => this.shoppingCartItems = _);
}
ngOnInit() {
}
In my HTMl I've tried using the async pipe line to unwrap the information
<p>You have {{shoppingCartItems.length}} items in your bag</p>
<ng-container *ngIf=" shoppingCartItems$ | async as shoppingCartStickers"></ng-container>
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
the output to the page shows <p>You have {{shoppingCartItems.length}} items in your bag</p> the amount of items in the cart but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag.
export class CartService {
private itemsInCartSubject: BehaviorSubject<ISticker[]> = new BehaviorSubject([]);
private itemsInCart: ISticker[] = [];
constructor() {
this.itemsInCartSubject.subscribe(_ => this.itemsInCart = _);
}
public addToCart(item: ISticker){
this.itemsInCartSubject.next([...this.itemsInCart, item]);
}
public getItems(): Observable<ISticker[]> {
return this.itemsInCartSubject.asObservable();
}
This is my cart.services.ts for reference. This is my first time posting to stack overflow so if there is anything that would be helpful to see please let me know as I'm happy to provide it.
Alternatively, you can have your ShoppingCartComponentComponent something like this:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartStickers: ISticker[] = [];
constructor(
private cartService: CartService
) { }
ngOnInit() {
this.cartService.getItems()
.subscribe(items => {
this. shoppingCartStickers = items;
});
}
}
Now, shoppingCartStickers can be directly used in the component html:
<p>You have {{shoppingCartStickers?.length}} items in your bag</p>
<ng-container *ngIf="shoppingCartStickers?.length">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
the output to the page shows You have {{shoppingCartItems.length}} items in your bag the amount of items in the cart
It's because you use shoppingCartItems which calculated in subscribe and will be up to date on every emitted event from shoppingCartItems$.
but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag
It's because the template variable shoppingCartStickers available only in the block you define it. So you just need to do:
<ng-container *ngIf="(shoppingCartItems$ | async) as shoppingCartStickers">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
I recommended you to use single variable for that. You don't need to do shoppingCartItems$ | async as shoppingCartStickers in template because this data has already stored in the shoppingCartItems property of the component.
Your issue is that you have misplaced the ending tag of <ng-container>.
What you have is essentially:
<ng-container>
<!-- Stickers available ONLY here -->
</ng-container>
<!-- Some code that tries to use stickers, but they are not available in this score --!>
The correct usage of ng-contaner is to actually wrap it AROUND the elements that require the data you're fetching from the service.
In any case, I would not use the ng-container here at all. You use case is:
You have an observable of an array: Observable<ISticker[]>
You want to create an element for each of the ISticker objects.
For this you can combine async pipe together with *ngFor:
<app-home *ngFor="let sticker of shoppingCartItems$ | async">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
Separately from this, you want to peek at the length of the array to display an extra p tag.
Since you already have the access to the observable, you can easily do
<p>You have {{(shoppingCartItems$|async)?.length}} items in your bag</p>
<!-- OR -->
<p *ngIf="shoppingCartItems$|async as items">
You have {{items.length}} items in your bag
</p>
The problem with this is that now you're accessing shoppingCartItems$ TWICE: one time for the list of items, and one time to get the length. If you're getting this data from an API, this can easily result in TWO requests.
To combat this problem, you can use shareReplay operator, which will allow multiple subscribers to use the same value from a single observable:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]>;
constructor(private cartService: CartService) {
}
ngOnInit() {
this.shoppingCartItems$ = this.cartService.getItems()
.pipe(shareReplay(1));
}
}
Here's a stackblitz with this example, where you can see how this works.
Now you are free to use shoppingCartItems$ any number of times, and this won't cause any unintended behaviour.
On top of that, now we don't manually subscribe, which means that the async pipe will get rid of the subscription for us when the component is destroyed, preventing a potential memory leak.
I have a service which has following method:
room-service.ts
#Injectable()
export class RoomService {
private readonly _equipment: BehaviorSubject<EquipmentDto[]> = new BehaviorSubject([]);
public equipment$ = this._equipment.asObservable();
getEquipmentForRoom(roomId: number) {
this.restService.getEquipmentForRoom(roomId).subscribe(res => {
this._equipment.next(res);
});
}
room-component.ts:
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable < EquipmentDto[] > ;
equipmentList: Array < EquipmentDto > ;
constructor(private equipmentService: EquipmentService) {}
ngOnInit() {
this.equipment$ = this.equipmentService.equipment$;
this.equipmentService.getEquipmentForRoom(this.room.id);
this.equipment$.subscribe(items => {
this.equipmentList = items;
});
room-component.html
<div *ngFor="let eq of room.equipmentList">
<!-- list my equipment here -->
</div>
Now I have a parent component which contains multiple Room Components (those are added programmatically based on the amount of rooms). Anyway, list of equipment is the same for each of the rooms. It looks like once subscribed, the data in first components is overwritten by the component created as the last one.
My question is, how can I get a proper data for each of the rooms using the observable from my service?
You can use this approach with single BehaviorSubject only when your data is the only source of this data.
Instead, you can change your getEquipmentForRoom(roomId: number) like this:
getEquipmentForRoom(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
And then subscribe to it in the compoment:
this.equipmentService.getEquipmentForRoom(this.room.id).subscribe(items => {
this.equipmentList = items;
});
And I agree with Alexander, this component should be dumb as possible.
try to build pipes instead of subscriptions
#Injectable()
export class RoomService {
// return an observable.
getEquipmentForRoom$(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable<EquipmentDto[]>;
constructor(private equipmentService: EquipmentService){}
ngOnInit() {
// simply share observable.
this.equipment$ = this.equipmentService.getEquipmentForRoom$(this.room.id);
});
<div *ngFor="let eq of equipment$ | async"> <!-- add async here -->
<!-- list my equipment here -->
</div>
You can fetch the data once in your parent and pass the data to your child components.
That child component (RoomsComponent) should be dumb and not doing requests
Adding onto Alexander’s answer.
The problem is that you are subscribing to the same BehaviourSubject for all the components and the components are taking the latest roomData that is emitted by the BehaviourSubject, which would be the last RoomComponent.
It would work if the BehaviuorSubject holds all the rooms which are fetched by the parent component, and passing each room data to each RoomComponent using #Input.
In my application when I click on a button I would like to display a spinner.
I get a simple CSS spinner from this url : https://projects.lukehaas.me/css-loaders/
I added css to my main.css file, and after that I've added this line of code in my
app.component.html :
<div class="loader">Loading...</div>
The reason why I choosed app.component.html is that I would like to make this spinner available everywhere in my application..
But for now on button click I'm getting list of persons from database. And this is how it looks (my component):
export class PersonComponent implements OnInit {
private modalId: string;
private persons: Array<Person>;
constructor(private personsService: PersonsService) {}
ngOnInit() {}
// Here I'm getting data from database and I would like to display spinner/loader until all of data is loaded
show() {
$('#' + this.modalId).modal('show');
this.personsService.getAll().do(data=>console.log(data)).subscribe(persons => this.persons = persons);
}
}
Here is my app.component.html
<router-outlet></router-outlet>
<div class="loader">Loading...</div>
And here is my PersonsService.ts which is responsible for fetching data from database:
#Injectable()
export class PersonsService {
public showSpinner: boolean = false;
constructor(private _http: HttpClient) { }
getAll(): Observable<Person[]> {
this.showSpinner(); //Cannot invoke expression whose type lacks a call signature. -> I got this error here
return this._http.get<Person[]>(apiUrl)
.catch(
(error: HttpErrorResponse) => {
return Observable.throw(error);
});
// Somewhere here I would hide a spinner? Is this possible, I mean will this be shown when data is fully loaded or ?
}
displaySpinner() {
this.showSpinner = true;
}
hideSpinner() {
this.showSpinner = false;
}
I need some if condition probably in my app.component.html so it might looks like this? :
<div *ngIf="personsService.showSpinner" class="loader">
Loading...
</div>
Or I need to do something like this? :
<div *ngIf="personsService.showSpinner">
<div class="loader">Loading...</div>
</div>
However this is not working now and I don't know how to achieve this, to simply show spinner when service is getting data from database, and to hide it when all of the data is fetched..
Thanks guys
Cheers
You can show hide and know the data base loading success and reading Full response using angular observe option
getAll(): Observable<HttpResponse<Person[]> {
return this._http.get<Person[]>(apiUrl,{ observe: 'events' })
.catch(
(error: HttpErrorResponse) => {
return Observable.throw(error);
});
// Somewhere here I would hide a spinner? Is this possible, I mean will this be shown when data is fully loaded or ?
}
You can set the spinner on service or you can change the status of spinner inside components where you subscribe the getAll() observable example
components.ts
this.service.getAll().subscribe((response:HttpEvent<Object>)=>{
//console.log(response);
//import the HttpEventType from #angular/common/http
if(response.type==HttpEventType.DownloadProgress){
this.showSpinner = true;
}
if(response.type==HttpEventType.Response){
this.showSpinner = true;
}
})
Modify your service to use a BehaviorSubject instead of a plain boolean and take advantage of the rxjs pipe and tap operator so that you can set the spinner flag in the service itself, instead of toggling in every service consumer component.
#Injectable()
export class PersonsService {
public showSpinner: BehaviorSubject<boolean> = new BehaviorSubject(false);
public readonly apiUrl = "https://api.github.com/users";
constructor(private _http: HttpClient) { }
getAll(): Observable<Person[]> {
this.showSpinner.next(true);
return this._http.get<Person[]>(this.apiUrl).pipe(
tap(response => this.showSpinner.next(false),
(error: any) => this.showSpinner.next(false))
);
}
}
You can use the async pipe to access showSpinner
<div class="loader" *ngIf="_personService.showSpinner | async">
</div>
<div *ngIf="!(_personService.showSpinner | async)">
<button (click)="foo()">Click here</button>
<pre>
{{response | json}}
</pre>
</div>
Demo