How to update tab component after submit - javascript

`I have an Angular 6 app using Bootstrap JS Tab. One of my tabs contains a list of notes. The user adds a note through a modal popup, and the list is refreshed with the new note. That works fine. However, in the header of the tab, I have an anchor tab reflecting the number of notes entered. My question is, how can update that number when a new note is added?
The app is arranged as so: There is a user-details.component.html that displays all the tabs. The notes tab is contained inn user-notes.component.html and there's a user-notes.component.ts (posted below).
For example, here's the html of some of the tabs in user-detail.component.html:
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
<li class="active">Entitlements</li>
<li>Payment Instruments</li>
<li><a href="#notes" data-toggle="tab" >Notes ({{_notes.length}})</a></li> <!--style="display: none" -->
</ul>
Notice that the "Notes" link references {{_notes.length}}. I need to update _notes.length when I post, but I'm totally unsure how. Can someone help?
EDIT: Here's my component code:
import { AuthGuard } from '../../service/auth-guard.service';
import { Router } from '#angular/router';
import { Logger } from './../../service/logger.service';
import { Component, OnInit, Input } from '#angular/core';
import { UserDetailService } from '../../user/service/user-detail.service';
import { UserEntitlementService } from '../../user/service/user-entitlement.service';
import { Note } from '../../user/model/note.model';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-notes-component',
templateUrl: './user-notes.component.html'
})
export class UserNotesComponent implements OnInit {
#Input() asRegIdofUser;
#Input()
private notesModel: Note[]=[];
private actionResult: string;
private notesCount: number;
private currentNote: Note;
constructor(private _logger: Logger, private _userDetailService: UserDetailService,
private _router: Router, private _userEntitlementService: UserEntitlementService,
private authGuard: AuthGuard) {
}
ngOnInit(): void {
//read data....
this.currentNote= new Note();
if (this.asRegIdofUser)
this.refreshNotesData();
}
refreshNotesData(){
this.actionResult='';
this._userDetailService.getNotes(this.asRegIdofUser).subscribe(
responseData =>{
let embedded = JSON.parse(JSON.stringify(responseData));
let notes = embedded._embedded.note
this.notesModel=[];
notes.forEach(note => {
this.notesModel.push(note);
})
this.notesCount=this.notesModel.length;
},
error =>{
this._logger.error("error on loading notes "+error);
}
)
this.currentNote= new Note();
}
onCreateNote(notesModal){
this._userDetailService
.postNote(this.asRegIdofUser,this.currentNote).subscribe(
response => {
if (response==='OK')
this.actionResult='success';
else
this.actionResult='failure';
},error => {
this.actionResult='failure';
}
)
}
userHasEditRole(): boolean{
return this.authGuard.hasAccess('edituserdetails');
}
onDelete(noteId: string){
let deleteNoteId: number = Number.parseInt(noteId);
this._userDetailService.deleteNote(this.asRegIdofUser,deleteNoteId).
subscribe(
response =>{
if(response == 'OK')
this.refreshNotesData();
},
error =>{
this._logger.error("error on deleting notes "+error);
}
)
}
}

Create a DataService, that will have your private listOfItems, a private BehaviorSubject that can be used to notify other components about changes in the list and the same, exposed as a public Observable.
import { Injectable } from '#angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
#Injectable()
export class DataService {
private listOfItems: Array<string> = [];
private list: BehaviorSubject<Array<string>> = new BehaviorSubject<Array<string>>(this.listOfItems);
public list$: Observable<Array<string>> = this.list.asObservable();
constructor() { }
addItemToTheList(newItem: string) {
this.listOfItems.push(newItem);
this.list.next(this.listOfItems);
}
}
Inject this service in all the three Components, the Header, Add and List. And use it accordingly.
Here's a Working Sample StackBlitz for your ref.

Here you are trying to communicate between different angular components.
For this, You can use a service or listen to an event emitted from the component that adds the note.
You can find more info here: component-interaction

Related

How can I avoid making multiple get request in this Angular app?

I am working on an e-commerce app in Angular 11.
I have a service that makes a get request and reads a JSON.
The purpose of this service is to determine which product is promoted.
The service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Campaign } from '../models/campaign';
#Injectable({
providedIn: 'root'
})
export class PromoProductsService {
public apiURL: string;
constructor(private http: HttpClient) {
this.apiURL = `${apiURL}/promo-products`;
}
public getPromoData(){
return this.http.get<Campaign>(`${this.apiURL}/campaign`);
}
}
In the product card component I have:
public getPromoData() {
this.PromoProductsService.getPromoData().pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.campaignData = data;
this.campaignProducts = this.campaignData.campaign.products;
let promoProduct = this.campaignProducts.find((product:any) => {
return this.product.product_id == product.id;
});
if (promoProduct) {
this.isCampaignProduct = true;
this.cdr.detectChanges();
}
});
}
The problem
The code above checks, for every product card, if the product is in the array of promoted products.
The problem with this is that there is a request for the array of promoted products for every product on the page.
Question:
How can I make (and use) a single request for the array of promoted products?
You should share the result of your HTTP request to all components who need it.
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Campaign } from '../models/campaign';
#Injectable({
providedIn: 'root'
})
export class PromoProductsService {
public apiURL: string;
promo$: Observable<Campaign>;
constructor(private http: HttpClient) {
this.apiURL = `${apiURL}/promo-products`;
this.promo$ = this.http.get<Campaign>(`${this.apiURL}/campaign`).pipe(shareReplay());
}
}
This observable can the be used by different components in order only perform one single HTTP call (on first subscription).
In your component you can adjust your code to do the following:
public getPromoData() {
this.PromoProductsService.promo$.pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.campaignData = data;
this.campaignProducts = this.campaignData.campaign.products;
let promoProduct = this.campaignProducts.find((product:any) => {
return this.product.product_id == product.id;
});
if (promoProduct) {
this.isCampaignProduct = true;
this.cdr.detectChanges();
}
});
}
There are several approaches, just to name 2 I would recommend:
make use of shareReplay rxjs operator
Call the service from the parent, that holds all the products and provide the whole list to the child, so the child is pretty much dumb

In Angular 9, how do I update a component's data field to show in the DOM without re-instantiating it?

I'm fairly new to Angular 9. I have a program where a user enters in a name - which, upon submitting - a POST HTTP request is sent and the name is stored. I then have an unrelated component for a sub-header that lists the names that have been stored using a GET HTTP request using ngOnInit(). However, I need the sub-header to update that list of names dynamically each time a new list is entered rather than just whenever the component instantiates.
I'm unsure how to proceed. I'm sure I could simply add a button that fetches and updates said list, but trying for something more dynamic. Thanks in advance!
//SERVICE.TS...
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { NewList } from './new-list.model';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class ListService {
createdLists: NewList[] = [];
constructor(private http: HttpClient) { }
createList(postData) {
return this.http
.post(
'API_KEY',
postData
);
}
getLists() {
return this.http
.get<NewList>(
'API_KEY'
).pipe(map(responseData => {
const responseArray: NewList[] = [];
for (const key in responseData) {
responseArray.push(responseData[key])
}
return responseArray;
})
);
}
}
// NEW-LIST-MENU.TS (USER ENTERS A NAME)...
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { UIService } from 'src/app/shared/ui.service';
#Component({
selector: 'app-new-list-menu',
templateUrl: './new-list-menu.component.html',
styleUrls: ['./new-list-menu.component.css']
})
export class NewListMenuComponent implements OnInit {
constructor(private listService: ListService,
private uiService: UIService,
private router: Router) { }
ngOnInit(): void {
}
onSubmit(form: NgForm) {
const listName = form.value.listname;
const newListObj = new NewList(listName, []);
this.listService.createList(newListObj)
.subscribe(() => {
this.router.navigate(['']);
});
const lists = this.listService.updateLists(newListObj);
form.reset();
}
onCancel() {
this.router.navigate(['']);
}
}
// SUB-HEADER.TS...
import { Component, OnInit, Output } from '#angular/core';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { faWindowClose } from '#fortawesome/free-solid-svg-icons';
import { faPlusCircle } from '#fortawesome/free-solid-svg-icons';
import { faList } from '#fortawesome/free-solid-svg-icons';
import { faSignOutAlt } from '#fortawesome/free-solid-svg-icons';
import { Subject } from 'rxjs';
#Component({
selector: 'app-sub-header',
templateUrl: './sub-header.component.html',
styleUrls: ['./sub-header.component.css']
})
export class SubHeaderComponent implements OnInit {
createdLists: NewList[];
faWindowClose = faWindowClose;
faPlusCircle = faPlusCircle;
faList = faList;
faSignOutAlt = faSignOutAlt;
#Output() closeSub = new Subject();
constructor(private listService: ListService,
private router: Router) { }
ngOnInit(): void {
this.listService.getLists().subscribe((responseData) => {
this.createdLists = responseData;
});
}
onCloseSelect() {
this.closeSub.next();
}
onNewListSelect() {
this.onCloseSelect();
this.router.navigate(['new-list-menu']);
}
onLogOutSelect() {
}
}```
You can accomplish this in many ways, as these components are not related to each other, you can introduce a state service and use observables. see below possible solution
Create a new state service ListStateService
export class ListStateService {
private listData = new BehaviorSubject<NewList >({} as NewList);
listData$ = this.listData .asObservable();
}
Inject ListStateService into NewListMenuComponent
In the onSubmit, after you update,
const lists = this.listService.updateLists(newListObj);
this.listData .next(lists );
Inject ListStateService into SubHeaderComponent
In the ngOnInit(), subscribe to the ListStateService.listData$ and here you will get the value on changes
In your service, use an event emitter (very useful):
import { EventEmitter } from "#angular/core";
#Output() myEvent: EventEmitter<any> = new EventEmitter();
then emit new data to your sub header component through your service like so:
emitEvent (newData: Array<string>) {
this.myEvent.emit({
data: newData,
});
}
Subscribe to new data in your sub header component ngOnInit and use it:
this.myService.myEvent.subscribe((newData: Array<string>) => {
console.log(JSON.stringify(newData.data));
});
Note: Subscriptions will cause memory leaks if constantly re-subscribed in the component, so you can save the subscription and call unsubscribe() on it in the ngOnDestroy callback.
It's a little unclear what you are trying to do, but if you are trying to pass data from a parent component to a child component, you can do this either with Input fields or a ViewChild
to use Input fields your parent might looks like this:
<app-sub-header [names]="names"></app-sub-header>
then use an "Input" field in the child. Updating names in the parent should update the same named variable in the child in real time.

How to call a function from another component [Angular]

I'm new to Angular. This question is very much similar to mine but doesn't answers my question. In my case, the two components are stand-alone, that means they ain't parent-child to each other. They're separate and at same directory level. That's why my problem is different from the rest. I tried using:
#ViewChild();
#Output() ....EventEmitter()
But I'm still getting an error in navbar.component.ts:
ERROR TypeError: "this.articles is undefined"
My first component is NavBar and my second component is Articles
So the scenarioa is, pressed() method of NavBar have to call callSearch(arg) method of Articles.
Here is my navbar.component.ts
import { ..., ViewChild } from '#angular/core';
import { ArticlesComponent } from '../articles/articles.component';
#Component({
...
})
export class NavbarComponent implements OnInit {
text='something';
#ViewChild(ArticlesComponent, {static: false}) articles: ArticlesComponent;
constructor() { }
/* This method is called on click event from HTML page*/
pressed(text: string) {
this.articles.callSearch(text);
}
ngOnInit() {
}
}
navbar.component.html
<li class="nav-item">
<button class="btn" (click)="pressed(text)">Search</button>
</li>
And here is the component whose method I want to call from NavBar.
articles.component.ts
import { ... } from '#angular/core';
#Component({
...
})
export class ArticlesComponent implements OnInit {
articles = [];
filteredArticles=[];
constructor(...) { }
ngOnInit() {
...
}
/* this is called from navbar typescript and it will call SearchFunction below*/
callSearch(text: string) {
this.SearchFunction(text);
}
SearchFunction(text: string) {
this.filteredArticles=(this.articles.filter(e => {
/* some logic*/
}));
}
}
articles.component.html
<div *ngFor="let article of filteredArticles; let i = index;">
<div class="card text-center">
<div class="card-body">
<!-- card design related html -->
</div>
</div>
</div>
Please correct me.
PS: Here is the stackblitz.
To communicate two components that are not related to each other, you can use a service.
#Injectable({
providedIn: 'root',
})
export class YourService {
private yourVariable: Subject<any> = new Subject<any>();
public listenYourVariable() {
return this.yourVariable.asObservable();
}
private yourVariableObserver(value : type) {
this.yourVariable.next(value);
}
You import in yours components where you want use it this service.
import{ YourService } from ...
In component you want write the data call:
this.yourService.yourVariableObserver(yourData);
while where you want read the data:
this.yourService.listenYourVariable().subscribe(
variable => {
this.data = variable;
this.callyourFunction();
}
)
Obviously you can simple call the function without pass any value.

Angular4 - let multiple unrelated components notify each other of the problem of updating data, and whether there is a cleaner coding method?

I have encountered a project in progress, let multiple unrelated components notify each other of the update data, is there a cleaner coding method?
There are 3 components (more likely later) and a common-data component. They have no parent-child relationship with each other and only show on the same screen.
The desired effect is to press the button of any component, update the contents of common-data, and notify yourself and other components to fetch new messages from common-data.
At present, my approach is to use Rx's Observable and Subscription, but they must be imported in the component.ts and service.ts files of each component, and a lot of duplicate code appears, it is very messy, I don't know what is better. practice?
Thanks!
My code :
The sample name is test-a-comp (a.b.c and so on, the code is the same)
test-a-comp.html
<p>
{{ownMessage}}
</p>
<button (click)="sendChange()">update</button>
test-a-comp.component
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { CommonData } from '../common-data/common-data';
import { TestACompService } from './test-a-comp.service';
import { TestBCompService } from '../test-b-comp/test-b-comp.service';
import { TestCCompService } from '../test-c-comp/test-c-comp.service';
#Component({
selector: 'app-test-a-comp',
templateUrl: './test-a-comp.component.html',
styleUrls: ['./test-a-comp.component.css']
})
export class TestACompComponent implements OnInit {
subscription: Subscription;
ownMessage;
constructor(
private testAService: TestACompService,
private testBService: TestBCompService,
private testCService: TestCCompService,
) {
this.subscription = this.testAService.getMessage()
.subscribe((test) => {
CommonData.message = test;
});
this.subscription = this.testBService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
this.subscription = this.testCService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
}
ngOnInit() {
}
sendChange() {
this.testAService.sendMessage();
}
}
test-a-comp.service:
import { Injectable } from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
#Injectable()
export class TestACompService {
subscription: Subscription;
private subject = new Subject<any>();
constructor() {
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
sendMessage(): void {
this.subject.next('update message from A');
}
}
As far as i understand & you've mentioned in the above, there is a button in one of the component (test-a-component.html). If you update the button, you need to send message to other components which are subscribed.
The Components which have no Parent-Child relationship can communicate via a service:
Create a single service file (In your case: test-a-comp.service)
Create a Subject on what data you need to communicate via this service:
export class testMessageService {
constructor() {}
// Observable string sources
private message = new Subject<string>();
//Observable string streams
testMessage$ = this.message.asObservable();
constructor() {}
// Method to send message when a button is clicked
sendMessage(message: string) {
this.message.next(message);
}
/* You don't need "getMessage()" method as you've already subscribed to
the observables. There subscribed Observable string streams are
injected in your components (As below point 3) to display / do other
operation on the message. */
}
In your other Components, where you want to receive messages, do the following:
export class TestComponent 1 {
myMessage1: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage1 = message;
});
}
export class TestComponent 2 {
myMessage2: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage2 = message;
});
}
export class TestComponent 3 {
myMessage3: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage3 = message;
});
}
For more information/guidance refer Component interaction via a common
service: https://angular.io/guide/component-interaction
Hope this helps!

Auth0 service unable to find container using Angular 2

I'm creating an Angular 2 SPA for learning purposes and integrating Auth0 for handeling the authentication. I have an auth.service.ts that is going to be called from difference places in my application, for example in the top-navbar to logout and on the auth-page to handle logins and registrations.
When trying to place the Auth0 container in a div by setting the container option I get the following error: Can't find element with id auth-container
How can I let the auth.service know how/where to look for the auth-container div? Placing all the logic inside the auth.component.ts is assumably not an option because the auth.service will be used for other functionality in other places where the lock variable is also used.
auth.service.ts
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
declare var Auth0Lock: any;
var options = { container: 'auth-container' };
#Injectable()
export class Auth {
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, options);
constructor() {
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
});
}
public authenticated() {
return tokenNotExpired();
};
public logout() {
localStorage.removeItem('id_token');
};
}
auth.component.ts
constructor(public auth: Auth) {
auth.lock.show();
}
auth.component.html
<div id="auth-container"></div>
Well they did not make your life easy but by mistake I made it work.
Try this:
auth.component.ts
ngOnInit() {
this.auth.login()
}
Delete this from your constructor
auth.lock.show();
The auth.service is not a container, it's a service that provides a popup when the login function is invoked.
So, to reuse it wherever you like, you need to inject the auth service into the component where you want to call the auth service from. Then, you just call the method. For example, here is the html for my Start component. You can see that the click event for the signin button is bound to the "submitLogin()" method of the component (the Start component):
<div class="splash-back" *ngIf="!authService.authenticated()">
<div id="splash">
<div id="logo"><span class="silver">GCO</span>TeamKeeper
<p class="silver tagline">The other teams could make trouble for us if they win.</p>
<p class="silver attribution">~ Yogi Berra</p></div>
<div class="call">
<br>
<button class="btn-sign-in" (click) = "submitLogin()">Sign up or Log in</button>
</div>
<!--<mtm-authentication></mtm-authentication>-->
</div>
</div>
And here is the start component code (note the injection of the authentication service in the constructor):
#Component({
selector: 'tk-start',
templateUrl: './start.component.html',
styleUrls: ['./start.component.css']
})
export class StartComponent implements OnInit {
constructor(private authService: UserAuthenticationService) { }
ngOnInit() {
}
submitLogin(){
this.authService.login();
}
}
And to make this example complete, here is my auth service code:
import {Injectable} from "#angular/core";
import { tkConfig } from './user-authentication.config';
import {Router} from "#angular/router";
import {tokenNotExpired} from "angular2-jwt";
let Auth0Lock = require('auth0-lock').default;
#Injectable()
export class UserAuthenticationService {
// Configure Auth0
userProfile: Object;
lock = new Auth0Lock (tkConfig.clientID, tkConfig.domain, {
avatar: null,
theme: {
primaryColor: "#69BE28",
foregroundColor: "#000000"
},
languageDictionary: {
title: "GCO TeamKeeper"
}
}
);
constructor(
private router: Router) {
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
this.router.navigate(['/organization']);
});
})
}
public login() {
// Call the show method to display the widget.
this.lock.show();
};
public authenticated() {
// Check if there's an unexpired JWT
// It searches for an item in localStorage with key == 'id_token'
return tokenNotExpired();
};
public logout() {
// Remove token from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
this.router.navigate(['/start']);
};
}

Categories

Resources