I am fairly new to Angular and I'm trying to display async data from my service into my component. When I do this, the data seems to update, but the component only shows the new data after I click a button. It seems to me the DOM is not updated when data is changed in my service, and only updates when I tell it to, in this example on the click of a button.
My app.component.ts :
import { Component } from '#angular/core';
import { AuthserviceService } from './services/authservice.service';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
displayName: string = 'no displayName available';
subscription: Subscription;
constructor(public authService: AuthserviceService){}
ngOnInit(){
this.subscription = this.authService.getDisplayName().subscribe(displayName => {this.displayName = displayName})
}
ngOnDestroy(){
this.subscription.unsubscribe();
}
login(){
this.authService.login();
}
logout(){
this.authService.logout();
}
methodThatDoesNothing(){
}
}
My app.component.html :
<button (click)="logout()">Logout</button>
<button (click)="login()">Login</button>
<button (click)="methodThatDoesNothing()">button that does nothing</button>
<p>{{ displayName }}</p>
<router-outlet></router-outlet>
My authservice.service.ts :
import { Injectable } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/auth';
import * as firebase from 'firebase/app';
import { Observable, Subject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class AuthserviceService {
private displayName = new Subject<any>();
constructor(public afAuth: AngularFireAuth) { }
login() {
this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()).then(data => {
this.displayName.next(data.user.displayName);
});
}
logout() {
this.afAuth.auth.signOut();
}
getDisplayName(): Observable<any> {
return this.displayName.asObservable();
}
}
I am trying to authenticate by using Google (popup shows up, I login, data is provided by Google (including displayName) ). I then want to display my display name, which is saved in the service, into my component. Before logging in, this is what I see:
When I click login, I can login, but the displayname is not updated. Only when I click the button "button that does nothing" (or any other button), the display name is updated:
It seems to me the DOM is changed on the click of a button, but I don't really know what is actually happening behind the scenes ( I coulnd't find the answer, mainly because I did not know what to look for).
Am I doing something wrong in my code?
Am I forgetting something?
How would I correctly display the displayName, after logging in?
Thanks in advance.
EDIT:
This piece of code is the problem I figured, I don't know why yet:
login() {
this._displayName.next('this data will be shown on the template immediately');
this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()).then(data => {
this.displayName.next(data.user.displayName); //This data here does not
});
}
The first "displayname.next" changes the displayName and immediately changes the value on csreen.
The second one, in the popup method, does not show immediately, and requires me to click a button (which forces a refresh???) to show the displayname.
I would recommend that you use BehaviorSubject instead of Subject.
A BehaviorSubject holds one value. When it is subscribed it emits the value immediately. A Subject doesn't hold a value.
private displayName: BehaviorSubject<string> = new BehaviorSubject('no displayName available');
create a get function
get _displayName(){
return this.displayName
}
subscribe it in the template with async pipe
<p>{{ authService._displayName | async }}</p>
when you want to pass a new value to it just use
_displayName.next('new value')
or if outside of service
authService._displayName.next('new value')
async pipe will handle subing and unsubscribing for you.
So you can remove the subscription on OnInit.
Ive made a stackblitz example that comes close to yours to showcase this
https://stackblitz.com/edit/angular-effvqv
The way you did it was a bit off becouse
Observable → Subject → BehaviorSubject
so you make an Observable out of an Observable.
Hope this helped.
I have "fixed" the problem by forcing it to detect a change. I first tried ChangeDetectorRef, but that is only a solution when used inside of the component, while using a service, I used the solution mentioned in this topic (which uses ApplicationRef):
Trigger update of component view from service - No Provider for ChangeDetectorRef
Related
I'm using Angular 11, Angular material and Bootstrap for a project, my problem is I want to show a pop up with and ad the first time a user loads the home page, the modal dialog is made in angular material, I have the modal dialog in ads component and then call it in the home component on the ngOnInit so the dialog will show when the user loads the home page, I found some solutions using JS but didn't make them work for me, any help on how can I solve this?
My ads component html, I'm only showing the image, no button to close the modal but if I need to add a button for a solution, I can add the button.
<mat-dialog-content id="myModal" class="gradient-border">
<img style="max-width: 100%" src="../../../assets/img/modal-ad.jpg" />
</mat-dialog-content>
ads component ts
import { Component, OnInit } from '#angular/core';
import { MatDialogRef } from '#angular/material/dialog';
#Component({
selector: 'app-anuncios',
templateUrl: './anuncios.component.html',
styleUrls: ['./anuncios.component.css'],
})
export class AnunciosComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<AnunciosComponent>) {}
ngOnInit(): void {}
}
Home component ts
import { Component, OnInit } from '#angular/core';
import { MatDialog } from '#angular/material/dialog';
import { AnunciosComponent } from '../anuncios/anuncios.component';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
})
export class HomeComponent implements OnInit {
constructor(public dialog: MatDialog) {}
ngOnInit(): void {
this.showDialog();
}
showDialog() {
const dialogRef = this.dialog.open(AnunciosComponent, {
maxWidth: '100vw',
maxHeight: '150vw',
panelClass: ['animate__animated', 'animate__bounceInDown'],
});
}
}
So this code makes the modal dialog always show when the home page is load, I need to show it the first time the user load the home page, I saw some solutions using cookies or JS functions but didn't make them work for me, as I said, I'm new to this and I think I didn't use those solutions properly for my project, any suggestions will be appreciated.
If in order to achieve what you mean you need to "remember" if the user has already seen the dialog.
Now I have another question for you:
Does the user need to see this every time the page loads?
Once and never again?
Every time he initiates a session on your site? (opens the browser to browse your site)
If your use-case is like option 1, then you might just have an internal variable. You can either use a static variable with a boolean or use a singleton Service from Angular.
#Component({...})
export class MyComponent {
public static hasAdvertBeenShown = false
ngOnInit() {
if(!MyComponent.hasAdvertBeenShown) {
this.matRef.showDialog()
MyComponent.hasAdvertBeenShown = true
}
}
}
If your use-case is to show the advertisement the first time the user browses your site then your variable would be stored inside localStorage and it would survive opening and closing the browser.
#Component({...})
export class MyComponent {
public static hasAdvertBeenShown = MyComponent.hasAdvertBeenShownBefore()
ngOnInit() {
if(!MyComponent.hasAdvertBeenShown) {
this.matRef.showDialog()
MyComponent.markAsSeen()
}
}
public static boolean hasAdvertBeenShownBefore() {
return JSON.parse(localStorage.getItem('advert'))
}
public static boolean markAsSeen() {
localStorage.setItem('advert', true)
}
}
If your use-case is the latter then use sessionStorage is similar to localStorage but it's shorter lived (per session)
#Component({...})
export class MyComponent {
public static hasAdvertBeenShown = MyComponent.hasAdvertBeenShownBefore()
ngOnInit() {
if(!MyComponent.hasAdvertBeenShown) {
this.matRef.showDialog()
MyComponent.markAsSeen()
}
}
public static boolean hasAdvertBeenShownBefore() {
return JSON.parse(sessionStorage.getItem('advert'))
}
public static boolean markAsSeen() {
sessionStorage.setItem('advert', true)
}
}
If you'd like to know more about local and session storages you can take a read over here
Here's what I usually do in my apps
Whenever I need to store something into localStorage, let that be preferences (user settings, dark-mode, etc.) and I want that to survive browser restarts I need to use localStorage.
Since using raw localStorage can get messy quite easily what I do is just create a singleton "UserSettingsService" that wraps the "low-level" local storage logic so I can share it across the codebase:
#Inject({providedIn: 'root'})
export class SettingsService {
private storage: Storage = localStorage // Change this based on your use-case
public markAdvertisementAsShown() {
this.storage.setItem('advert', true)
}
public boolean hasAdvertisementBeenShown() {
const safeBoolean = this.storage.getItem('advert') ?? 'false' // defaulting to false in case it didnt exist
return JSON.parse(safeBoolean)
}
}
And then on my other classes:
#Component({...})
export class SomeComponent {
hasAdvertBeenShown = this.adverts.hasAdvertisementBeenShown()
constructor(private matRef: Mat..., private adverts: AdvertService){}
ngOnInit() {
if(!this.hasAdvertBeenShown) {
// do something like showing the advert
this.adverts.markAdverisementAsShown()
}
}
}
It might seem a little overkill for a boolean but apps tend to get more and more complex. Like you'd like later you want to show 3 diferent advertisements and you need to be sure which one you've shown. Now you wouldn't be serializing the boolean but rather the advertisement object that has been shown. Logic gets more complex but because it's in a service you would only change it there and boom it's working again throughout your app!
EDIT: This is NOT a duplicate of how to retrieve the value from localstorage. My case was completely different, in fact, it was not an issue with the localstorage but with the way Angular works.
I'm working on an Angular7 application.
I have a component that sets a token to localStorage: window.localStorage.setItem("oauth_token", oauth_token)
and then redirect to the home page: this.router.navigate(['/'])
The root component now should be able to read from the localStorage window.localStorage.getItem("oauth_token"), but the value is null unless i manually refresh the page.
When I inspect the Chrome dev tool I can see the token in the local storage, so why do I need to refresh the page?
What can I do to overcome this?
It's nothing related to the local storage. Your root component needs a trigger or refresh to access the local storage or any change in the data.
When you redirect to the home page only the code from the home page component is executed and not from the parent or root component.
You need to declare a variable in the service file. So whenever you update the local storage you will have to update the value of the service file variable. You can access this variable in your root component. So if there is any change in the service variable a trigger will be fired and you can update the local storage. Check the link for a sample code https://github.com/resistcheat/angular-variable-as-observable
Create a Service File
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class CommonService {
data: any;
private items: BehaviorSubject<any> = new BehaviorSubject<any>(this.data);
items$: Observable<any> = this.items.asObservable();
constructor(private http: HttpClient) { }
setLocalStorageData() {// Call this function to set localstorage
localStorage.setItem("oauth_token", oauth_token)
this.data = oauth_token;
}
}
//Trigger will be fired when this.data changes. Add the below code to your root Component
constructor(private commonService: CommonService) { }
items: any;
subscription: Subscription; //import { Subscription } from 'rxjs';
ngOnInit() {
this.subscription = this.commonService.items$.subscribe(items => {
this.items = JSON.stringify(items));
console.log(localStorage.getItem("oauth_token"));
}
}
ngOnDestroy() {
this.subscription && this.subscription.unsubscribe();
}
try without window, like that:
localStorage.getItem("oauth_token")...
<side-nav [navTitle]="navTitle"></side-nav>
<router-outlet>
</router-outlet>
I have navigation bar at the root component. I created [navTitle] with #Input Decorator inside the side-nav component. side-nav component is placed in another component(root-component). However I want access [navTitle] and change from component which loaded inside the router-outlet acording to which component is loaded. How do I achieve that?
You can't pass any data to router-outlet as to regular component (at the current version of Angular it's not possible, may be it will be added in the future), so the following syntax is invalid:
<router-outlet [dataToPass]="'something'"></router-outlet>
In provided case, you can use services to share data between your components, and I think, that using observable is the best way, because you will get the updated version of data realtime:
data.service.ts
// Other service stuff
#Injectable()
export class DataService {
private navTitle$: BehaviorSubject<string> = new BehaviorSubject<string>('Default nav title');
public setNavTitle(newNavTitle: string): void {
// Sets new value, every entity, which is subscribed to changes (`getNavTitle().subscribe(...)`) will get new value every time it changes
this.navTitle$.next(newNavTitle);
}
public getNavTitle(): Observable<string> {
// Allow to `subscribe` on changes and get the value every time it changes
return this.navTitle$.asObservable();
}
}
side-nav.component.ts
// Other component stuff
export class SideNavComponent implements OnInit, OnDestroy {
public navTitle: string = '';
private getNavTitleSubscription: Subscription;
constructor(private _dataService: DataService) { }
ngOnInit() {
// Will update the value of `this.navTitle` every time, when you will call `setNavTitle('data')` in data service
this.getNavTitleSubscription = this._dataService.getNavTitle()
.subscribe((navTitle: string) => this.navTitle = navTitle);
}
ngOnDestroy() {
// You have to `unsubscribe()` from subscription on destroy to avoid some kind of errors
this.getNavTitleSubscription.unsubscribe();
}
}
And any component, which is loaded in that router-outlet:
any.component.ts
// Other component stuff
export class SideNavComponent implements OnInit {
private navTitleToSet: string = 'Any title';
constructor(private _dataService: DataService) { }
ngOnInit() {
// Set title from current component
this._dataService.setNavTitle(this.navTitleToSet);
}
}
In such case you don't really need to pass the value from root component to side-nav, because you already have a subscription in side-nav component and you will have access to the latest value. If you need navTitle in both root and side-nav components, you can just move the logic with subscription to root.
And here is the working STACKBLITZ.
You can use a service to communicate between components. I have created a short example which would give you a glimpse of how it can be done.
The service being a singleton, has only one instance and hence the properties remain the same.
Hope it helps.
https://stackblitz.com/edit/angular-paziug?file=src%2Fapp%2Fapp.component.ts
Is it even possible to let a service call an component Method?
myapp.component
export class MyAppComponent {
public value;
...
public setValue(payload){
this.value = payload;
}
}
myapp.service
#Injectable()
export class MyAppService {
private myAppComponent: MyAppComponent;
private apiClientService: ApiClientService
// ...
After i make an PUT http call, the body from the response is my new "value"
// ...
putValue(payload: JSON){
return this.apiClientService.putAPIObject(payload).then((response) => {
this.myAppComponent.setValue(response);
});
}
}
This results in an ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'setValue' of undefined.
Can someone explain what im doing wrong?
Thanks in advance.
EDIT:
Since people complain about my approach, im totally fine to start from scratch if someone can explain me what is the best way to handle this problem.
I get values from an api, change them and i put them back to the api. I dont want to make a get call again, so i get the new data i want in the response of the Put call.
The call goes from component --> component service --> apiclient service
I guess the problem is that i have an extra service between the start and end point.
EDIT 2: I tried to avoid the component service and maked it work for me with only component --> apiclient service
Even this soultion is working for me at the moment I kind of dislike it, because I have to Copy and Paste a lot of code for the Same "Operation" with other objects from my api. For example I maked it work for the Picture Component, but I also need this for my Movie Component. Usally its a bad thing if I write the same code often in a project, or not?
There are at least a couple ways to solve this, but hopefully this gives you a start. Open to feedback and corrections.
Use an Observable
Let the service own knowledge of the value changes and emit changes. The component listens to an EventEmitter on1 the service to react to value changes. (See also: Creating and returning Observable from Angular 2 Service)
MyAppService
import { Subject } from 'rxjs/Subject';
#Injectable()
export class MyAppService {
private valueSource = new Subject<any>();
public valueUpdate$ = this.valueSource.asObservable();
putValue(payload: JSON){
return this.apiClientService.putAPIObject(payload).then((response) => {
/** here **/
this.valueUpdate$.next(response);
});
}
}
MyAppComponent
export class MyAppComponent {
public value;
private valueSubscription;
constructor(private _myAppService: MyAppService) {}
ngOnInit() {
/** and here **/
this._myAppService.valueUpdate$.subscribe((p) => this.setValue(p));
}
...
public setValue(payload){
this.value = payload;
}
}
Register the component
Answering the original question, the idea is to register the component with the service so that it can call the component as needed. You could pull a references through dependency injection but wouldn't recommend it (e.g. what if your original component reference is destroyed?)
MyAppService
#Injectable()
export class MyAppService {
private myAppComponent: MyAppComponent;
/** here **/
registerMyApp(myApp: MyAppComponent) {
this.myAppComponent = myApp;
}
putValue(payload: JSON){
return this.apiClientService.putAPIObject(payload).then((response) => {
this.myAppComponent.setValue(response);
});
}
}
MyAppComponent
export class MyAppComponent {
public value;
/** and here **/
constructor(myAppService: MyAppService) {
myAppService.registerMyApp(this);
}
...
public setValue(payload){
this.value = payload;
}
}
Thanks AJT_82 for noting that Angular does not want developers using EventEmitters on the service: What is the proper use of an EventEmitter?.
I'm using angular 4 in my application and currently the user is able to make changes over multiple components(drag and drop, remove items, add items and etc...).
Now, for every user action there is a http requests via the relevant service that persist the changes on the DB.
There is a requirement that the user will be able to make this changes and only persist them once he done changing and pressed save. (it can be 10-50 actions from different types.)
How would you suggest refactoring the code in order to support that ? to make an array of user actions, and on save iterate over the array and make the relevant actions one by one, write some http middleware to hold all http calls until 'save' is pressed?
You should look into redux. This would allow a MVP programming model and works just fine with Angular. ng2-redux
Just so you know the problem you are facing has a name.
"Application State Management"
This can be solved via redux like libraries (redux/ rxjs-store rxjs-effects etc)..
Or you could just use plain rxjs BehaviourSubject or Subject as Observable.
here is a plunker example of using plain rxjs observables and angular services to achieve state management.
https://embed.plnkr.co/dEDJri4TziCS91oZiuHb/
TL;DR
This is the services
#Injectable()
export class AppStateService{
private _dataSaved = new Subject<string>();
public dataSaved$ = this._dataSaved.asObservable()
constructor() {}
dispatchSaveEvent(data: String){
this._dataSaved.next(data);
}
}
This is the component that will dispatch the save event
#Component({
selector: 'my-footer',
template: `
<button (click)="saveData($event)">Save</button>
`
})
export class Footer implements OnInit {
constructor(private appState: AppStateService) {}
ngOnInit() {}
saveData(e){
this.appState.dispatchSaveEvent("Some data to save here...");
}
}
This is how you consume the observable in every component that is interested that a saved has occurred
#Component({
selector: 'my-comp-1',
template: `
<h1>Component-1! {{savedDataRecived}}</h1>
`
})
export class Comp1 implements OnInit {
savedDataRecived = "";
constructor(private appState: AppStateService) {}
ngOnInit() {
this.appState.dataSaved$.subscribe(data=> this.handleSaveEvent(data))
}
handleSaveEvent(data: string){
this.savedDataRecived = data;
}
}