calling back end only for particular component in Angular - html

I have Tags Components in my project and I reused that component in other components. In my Tags component ngOnInit, I called backend to get all the existing tags. The problem I have right now is that call is applied to every other components even though the call is not needed at other components other than Edit Components. Since I only need the backend call to show existing tags just for Edit Components, I tried to move that call to Edit Components ngOninit but it didn't show me the existing tags anymore. I would be really appreciated if I can get any help or suggestion on how to fix this.
Tags Component TS
ngOnInit(): void {
this.tagService.getAllTagsByType('user').subscribe((normalTags) => {
this.loading = true;
if (normalTags)
this.allnormalTags = normalTags;
this.allnormalTags.forEach(normalTags => {
this.allTagNames.push(normalTags.tag);
});
this.loading = false;
})
}
If i add this call in Tags Component, it show me all the existing tags in drop down. I tried to move this to Edit component ngOnIt since I only want Eidt Component to use that call but It didn't show me existing tags anymore.
Tags.Service.ts
getAllTagsByType(tagType: any){
return this.http.get<Tag[]>(`${environment.api.chart}/tags/all/${tagType}`).pipe(first());
}

You could try to setup a flag to trigger the backend call using #Input.
tags.component.ts
import { Component, OnInit, Input } from '#angular/core';
export class TagsComponent implements OnInit {
#Input() getAllTags = false;
ngOnInit(): void {
if (this.getAllTags) { // <-- check here
this.tagService.getAllTagsByType('user').subscribe(
(normalTags) => {
this.loading = true;
if (normalTags)
this.allnormalTags = normalTags;
this.allnormalTags.forEach(normalTags => {
this.allTagNames.push(normalTags.tag);
});
this.loading = false;
},
error => {
// handle error
}
);
}
}
}
Now pass the value true to getAllTags when you wish to make the backend call. Since ngOnChanges hook is triggered before ngOnInit, the call will not be made if the property isn't passed in the component selector.
<!-- edit component -->
<mc-tags
[getAllTags]="true"
[workspace]="workspace"
[removable]="true"
[selectable]="true"
[canAdd]="true" ]
[editMode]="true"
(added)="tagAdded($event)"
(removed)="tagRemoved($event)"
> </mc-tags>
<!-- other components -->
<mc-tags [workspace]="workspace"></mc-tags>

Try to use RxJS. You should keep your Tags Data in TagService as a Subject (observable). Btw it is always best practise to store data in service layer.
TagService:
#Injectable({
providedIn: 'root'
})
export class TagService {
tagsSource = new BehaviorSubject<Tag[]>(null);
allnormalTags$ = this.tagsSource.asObservable();
getAllTagsByType(type: string){
http.request.subscribe(resultData => this.tagsSource.next(resultData))
}
}
Then in your component you can check whether data are already loaded and don't call it again.
export class ProductListComponent implements OnInit {
constructor(private tagService: TagService) { }
ngOnInit(): void {
if (isNullOrUndefined(this.tagService.tagSource.getValue())
this.tagService.getAllTagsByType('user')
}
P.S. You don't need to explicitly subscribe service observable in your component. Instead you can directly get your data from service subject/observable with async pipe.
<table *ngIf="tagService.allnormalTags$ | async as allnormalTags">
<tbody>
<tr class="product-list-item" *ngFor="let tag of allnormalTags">
<td data-label="name"> {{tag.name}} </td>

Related

Angular updating values across views

I have an Angular app that has the following:
One component has a text input and a button. The user fills in the text input and clicks the button. This updates a the URL for a router link.
The router link loads a component called view and it in turn reads the URL parameter from the router link and places that value in a service and displays it on the component so I know it worked.
So if the user type 'abc' in the text input then the router link URL would be /view/abc. So 'abc' will be displayed in the view component. Sometimes users will paste a router link like /view/def. This works to update the view component.
The part I can't get to work is to update the text input box in the other component to reflect the current value of the pasted link.
I tried using 'AfterViewChecked' to read the value from the service but that executes before the service value is updated so it is always incorrect.
These cannot bind to the same variable because this will eventually turn into a web service call and I don't want the service to be updated while the user is typing into the text input box, only when they click the button.
I'm not sure where else to look. Any searching I do just brings up data binding, but that is not my problem.
The relevant files are below but the full test sample code is on StackBlitz at https://stackblitz.com/edit/github-jwr6wj.
If you change the URL in the text input and click the button the URL display below will update. But if you paste in the pseudo URL https://github-jwr6wj.stackblitz.io/view/http%253A%252F%252Fwww.ebay.com%252F the URL displayed below will update correctly but I can't figure out how to update the text input to reflect what came in with the URL.
update.service.ts contains the URL that is the current one. This service will also load the data from a web service.
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class UpdateService {
url: string = "http://www.google.com/";
constructor() { }
}
view.component.ts is where the data selected by the user will be displayed. It parses the URL parameter for the data and updates the service with it.
import { ActivatedRoute, ParamMap } from '#angular/router';
import { UpdateService } from '../update.service';
#Component({
selector: 'app-view',
templateUrl: './view.component.html',
styleUrls: ['./view.component.css']
})
export class ViewComponent implements OnInit {
constructor(public activatedRoute:ActivatedRoute, public updateService: UpdateService) { }
ngOnInit(): void {
this.activatedRoute.paramMap.subscribe((paramMap: ParamMap) =>{
this.getUrl(paramMap);
});
}
getUrl(paramMap: ParamMap): void {
const incomingUrl = paramMap.get("url");
if (incomingUrl == null) {
this.updateService.url = "http://www.google.com/";
} else {
this.updateService.url = decodeURIComponent(incomingUrl);
}
}
}
view.component.html
<p>URL: {{updateService.url}}</p>
toolbar.component.ts is where the user will enter they request. sourceUrl is the variable that will be updated when the user types. However I also want it to update when the page is visited via the browser URL with the correct data as part of that URL. I can send data to the view component via the router but I can't find out how to send data back to the toolbar component.
import { UpdateService } from '../update.service';
#Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.css'],
})
export class ToolbarComponent implements OnInit {
sourceUrl: string = '';
constructor(private updateService: UpdateService) {}
ngOnInit(): void {
this.sourceUrl = this.updateService.url;
}
getViewUrl(): string {
return '/view/' + encodeURIComponent(this.sourceUrl);
}
}
toolbar.component.html
<div class="col-sm-12">
<input type="text" [(ngModel)]="sourceUrl" />
<a class="btn btn-primary" [routerLink]="getViewUrl()">
<span class="fa fa-eye"></span>
</a>
</div>
One way to share data between components is using a Service and Observables. Change your url in the Service to be BehaviorSubject with an initial value.
The way BehaviorSubject works is that you emit values from components to update the Observable in the Service. The BehaviorSubject behaves both as an Observer and Observable.
Essentially, an Observer is an object that listens to events, in this case, updating the URL. An Observable is an object that components listen to for updates or changes. In this case, the View Component listens to the BehaviorSubject for this update to the URL.
Service
export class UpdateService {
private url$ = new BehaviorSubject<string>('www.google.com');
public readonly url: Observable<string> = this.url$.asObservable();
constructor() {}
}
Toolbar Component
export class ToolbarComponent implements OnInit {
sourceUrl: string = '';
constructor(private updateService: UpdateService) {}
ngOnInit(): void {
this.updateService.url.subscribe((str) => {
this.sourceUrl = str;
});
}
getViewUrl(): string {
return '/view/' + encodeURIComponent(this.sourceUrl);
}
}
View Component
export class ViewComponent implements OnInit {
constructor(
public activatedRoute: ActivatedRoute,
public updateService: UpdateService
) {}
ngOnInit(): void {
this.activatedRoute.paramMap.subscribe((paramMap: ParamMap) => {
this.getUrl(paramMap);
});
}
getUrl(paramMap: ParamMap): void {
const incomingUrl = paramMap.get('url');
if (incomingUrl == null) {
this.updateService.url.next('http://www.google.com/');
} else {
this.updateService.url.next(decodeURIComponent(incomingUrl));
}
}
}
View Component HTML
<p>URL: {{ updateService.url | async }}</p>
You are right to try with AfterViewChecked because it's just a timing issue. What you could do is have url inside updateService defined as a BehaviourSubject, so that at the moment it's updated in your view component, you see the change in the toolbar component.
Inside the service :
public url$: BehaviorSubject<string> = new BehaviorSubject("http://www.google.com/");
Inside the view component ts :
getUrl(paramMap: ParamMap): void {
const incomingUrl = paramMap.get("url");
if (incomingUrl == null) {
this.updateService.url$.next("http://www.google.com/");
} else {
this.updateService.url$.next(decodeURIComponent(incomingUrl));
}
}
And inside the view component HTML : (you can also subscribe to the Behaviour Subject directly inside the ts)
<p>URL: {{updateService.url$ | async}}</p>
And you will also have to deal with the fact that the url is a Subject inside the toolbar component ts!
Good luck, let me know if this is not clear!

How to load Loader on Every Route in Angular

Hello i want to put loader on every route link , when route link change show loader until all its component not load with api data.
Structure of Component
<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>
inside <router-outlet></router-outlet> i have other child component like and its data come from api.
<app-component1></app-component1>
<app-component2></app-component2>
so my problem is i cant put loader for page(route) wise if page load loader show and hide after all component load with dynamic(api) data
You can subscribe to RouterEvents to know when a route navigation is started to completed. You would also need a loader component in your AppComponent outside of your router-outlet and a flag to show/hide the loader. You can control loader visibility across the application using a LoaderService.
LoaderService
export class LoaderService {
isLoaderShown = new BehaviorSubject<boolean>(false);
constructor() { }
public showLoader() {
this.isLoaderShown.next(true);
}
public hideLoader() {
this.isLoaderShown.next(false);
}
}
You can use this LoaderService to show or hide the loader from anywhere in the application.
Update your AppComponent to add a new loader component. LoaderComponent just needs to contain a loader of your choice.
<app-header></app-header>
<app-loader *ngIf="showLoader"></app-loader>
<router-outlet></router-outlet>
<app-footer></app-footer>
Now we can control the loader display using the showLoader flag in AppComponent.
AppComponent
export class AppComponent implements OnInit {
showLoader = false;
constructor(
private loaderService: LoaderService,
private router: Router
) { }
ngOnInit() {
this.loaderService.isLoaderShown.subscribe(isLoaderShown => this.showLoader = isLoaderShown);
this.router.events.subscribe(routerEvent => {
if (routerEvent instanceof NavigationStart) {
this.loaderService.showLoader();
} else if (routerEvent instanceof NavigationEnd) {
this.loaderService.hideLoader();
} else if (routerEvent instanceof NavigationCancel) {
this.loaderService.hideLoader();
// Handle cancel
} else if (routerEvent instanceof NavigationError) {
this.loaderService.hideLoader();
// Handle error
}
});
}
}
Now, whenever a router navigation starts, we are showing the loader and hiding when navigation ends.
However, this doesn't take the API load times into consideration. For that, you could remove the this.loaderService.hideLoader(); from the NavigationEnd and add it in your API call subscription. This is the reason why we add it as a service. You can inject it onto your API Service and hide loader.
this.httpClient.get(url).subscribe(result => {
// Perform operations with the result
this.loaderService.hideLoader();
});
So, loader will be shown when navigation starts and when the API call result is available in service, loader can be hidden.

Issue with Angular BehaviourSubject which is not assignable to type error

I have a simple table with angular and typescript. I am sending table data from parent class to child class(which includes the table) and in this example data name is _domainData. It is taking the data correctly but I want to show it on table and I do not know how to assign it to my main table data variable domain_Data.
As in the example: if i say this.domain_Data = this._domainData;in ngOnInit() method.
#Component({
selector: 'mg-domain-display',
templateUrl: './domain-display.component.html',
styleUrls: ['./domain-display.component.scss']
})
export class DomainWhiteListingDisplayComponent implements OnInit {
private _domainData = new BehaviorSubject<Domain[]>([]);
displayedColumns = ['id', 'domain'];
domain_Data: Domain[] = [];
#Input()
set domainData(value: Domain[]) {
this._domainData.next(value);
}
get domainData() {
return this._domainData.getValue();
}
constructor(private globalSettingService: GlobalSettingsService, private dialog: MatDialog) {
}
ngOnInit() {
this.domain_Data = this._domainData;
}
}
And the error is Type:BehaviourSubject is not assignable to type Domain[]. Property 'includes'is missing in type 'BehaviourSubject'
As I said my main table data variable is domain_Data:
<mat-table #table [dataSource]="domain_Data">
You need to subscribe and get the value from BehaviorSubject
ngOnInit() {
this._domainData.subscribe((data) => this.domain_Data = data);
}
Alternatively, As few have commented, you can subscribe in the template using async pipe:
<mat-table #table [dataSource]="domain_Data | async">
Generally, if you don't need to deal with data in the component, it's best using async pipe, as it takes care of unsubscribe automatically.
I arrived a bit late but I would like to add 2 additional information about #Aragorn answer :
Be careful when using async pipe in the template of a component with ChangeDetectionStrategy.OnPush, as it will completly force components to trigger lifecycle detection changes as often as the default Strategy.
Second important info : don't forget to unsubscribe when your component is destroyed, or you will have subscription still up if you never resolve the BehaviourSubject (here you just do 'next' operations) :
subscription: ISubscription;
this.subscription = this._domainData.subscribe((data) => this.domain_Data = data);
then in onDestroy :
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}

How fixed method POST with angular 5

I develop application for Portal, but when i create new role,
the role is created but the problem is that the added element is not displayed, it must refresh the browser to display this new element !!!, what do I do to display the added element directly in my table , and how to develop the other methods (put and delete) and thank's (i develop this application with angular 5)
thid my code .html:
<form #personForm="ngForm" (ngSubmit)="onSubmit(personForm.value)">
<input name="RoleName" [(ngModel)]="RoleName">
<button type="submit">Save</button>
</form>
and this my code .ts:
export interface Role {
RoleName: string;
}
#Component({
selector: 'app-role',
templateUrl: './role.component.html',
styleUrls: ['./role.component.css']
})
export class RoleComponent implements OnInit, AfterViewInit {
private roles: any;
constructor(private _roleService: RoleService, private http: HttpClient) { }
onSubmit(role: Role) {
return this.http.post('http://172.16.47.34:8282/MonProject.webservices/api/Roles', role).subscribe(status => console.log(JSON.stringify(status)));
}
async ngOnInit() {
this.roles = await this._roleService.getRoles();
}
ngAfterViewInit(): void {
$('#example-table').DataTable({
pageLength: 10,
});
}
}
If your post is successful, then update your data table with the newly added role.
You can achieve this by adding this logic inside the subscribe function.
Put and delete should follow the same concept. If the response shows that the request was successfully processed, then (in the subscribe) you can modify your data table.
Two things I noticed:
Try to avoid using jQuery with Angular. These two are not the best
together. You can get a reference for the data table with
ViewChild.
Using async/await with ngOnInit is a bad practice.

How to make Angular 2 render HTML template after a promise in the component is resolved?

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