I am trying to interact between 2 components i.e. the header component and the router-outlet component which contains a reactive form. Some days ago after great research, I got an answer which told me to use shared service and there takes a ReplaySubject variable. This will act as an Observable which will be called in ngOnInit of the component in router-outlet. So the problem arises here. Everything is working fine but just now while testing noticed a small bug. After clicking the button in header the event is firing in the main component but it is staying active until I hard reload the page.
Below is my code:
shared.service.ts
public updateSrc = new ReplaySubject<any>(1);
public updateClick = this.updateSrc.asObservable();
header.component.ts (A button is clicked here)
update() {
return this.dataService.updateSrc.next(true);
}
main.component.ts (Here the main operation is happening on click of header button)
updateClicked = new Subject<any>();
ngOnInit() {
this.dataService.updateClick.pipe(takeUntil(this.updateClicked)).subscribe({
next: value => console.log(value); // At first click of button this happening perfectly but if coming from different route & had clicked the header button previously this thing is getting triggered
});
}
ngOnDestroy() {
this.updateClicked.next(); // Closing subscription here
}
Can anyone suggest to me how to reset this?
The ReplaySubject (and the BehaviorSubject) stores previously emitted values and emit those whenever a new subscriber is added.
If you don't want new subscribers to receive previously emitted value(s), you can always use Subject which doesn't store emitted values.
You may want to read more about the available variations in the docs.
Related
I have a component, lets call component 1. A method in component1 makes an axios.post request and the server returns a bunch of data. When data is loaded, a new button appears. When this button is clicked, it will be navigated to another route with another component, let call this component2. Now some of the loaded data from component1 needs to transferred to component2 and should be opened in new tab. Below is the code:
<script>
import axios from 'axios';
export default {
name: "CheckStandard",
data() {
return {
standard: '',
time: {},
programs: {},
example: {},
}
},
methods: {
checkData(){
let std= {
std: this.standard,
}
axios.post('http://localhost:3000/postdata', std)
.then(res => {
if (res.status === 200) {
if (res.data === 0) {
this.invalidID = "This Standard does not exist"
}
else {
let data = res.data
this.time = res.data["Starttime"];
this.programs = res.data["program"]
this.example = res.data["example"]
}
}).catch(error => {
console.log(error);
this.error = error.response
})
},
}
goToPictures(){
let route = this.$router.resolve({
name:'ProgramCheckList',
params: {
programs: this.programs,
time: this.time,
example: this.example
}
})
window.open(route.href,'_blank')
},
}
}
</script>
The function goToPictures is the function that is invoked after clicking the button. Now in this function goToPictures I have defined the route to navigate to another tab. But the problem the data in the params which it should carry is lost. I tried with $router.push ofcourse it works but it is not to open in new tab. Below is the code for the same:
goToPictures(){
this.$router.resolve({
name:'ProgramCheckList',
params: {
programs: this.programs,
time: this.time,
example: this.example
}
})
},
}
Since I am new to vue, I have tried my best to look for an answer for this, even I have came across some posts in several forums mentioning, it is may be not be possible even, instead advised to use vuex. But I still wanted to post it, maybe we have a solution now or any other idea. Thanks
The problem you're seeing stems from the fact that, when you open a new window, Vue is basically going to re-render your components as if you hit refresh. Your Component 2 has props that it can only inherit from another component, and as such, it has no possible way of knowing what the props it needs to use are.
To illustrate in simple terms what's happening:
The user navigates to Component 1. They click the button, which makes the GET request. You now have some data that you can pass onto Component 2 as props.
In a regular environment, the user would simply click on the link leading to Component 2, and the props would be passed on normally, and everything would work as intended.
The problem in your situation is that Component 2 depends on Component 1 for its data. By navigating directly to the Component 2 route (in this situation, opening a new window is functionally identical to a user copy/pasting the url into the adress bar), Vue never has the chance of interacting with Component 1, and never gets told where to get the props it needs to populate Component 2.
Overcoming the issue
There's a few things you can do here to overcome this issue. The most obvious one is to simply open Component 2 as you would normally, without opening a new window, but keep in mind that even if you do this, should a user copy/paste the URL where Component 2 is, they'll run into the exact same issue.
To properly deal with the issue, you have to specify a way for Component 2 to grab the data it needs. Since the data is already fetched, it makes sense to do this in the created() or mounted() hooks, though if you wanted to you could also deal with this in Vue Router's beforeRouteEnter() hook.
Vuex
While you don't necessarily need a state management tool like Vuex, it's probably the simplest way for your needs. When you grab the data from Component 1, store it and access it from the Component 2 mounted() hook. Easy-peasy.
localStorage()
Alternatively, depending on how the data is being served, you could use localStorage(). Since you're opening a new window, sessionStorage() won't work. Do note that localStorage() can only hold strings and nothing else, and isn't necessarily available in every browser.
You can store the data to a global variable and use that in the newly opened window. Asked here Can I pass a JavaScript variable to another browser window?
Provided the windows are from the same security domain, and you have a reference to the other window, yes.
Javascript's open() method returns a reference to the window created (or existing window if it reuses an existing one). Each window created in such a way gets a property applied to it "window.opener" pointing to the window which created it.
Either can then use the DOM (security depending) to access properties of the other one, or its documents,frames etc.
Another example from same thread:
var thisIsAnObject = {foo:'bar'};
var w = window.open("http://example.com");
w.myVariable = thisIsAnObject;
//or this from the new window
var myVariable = window.opener.thisIsAnObject;
EDIT 2: This appears to be my general problem, and solution (using setTimeout so Angular's lifecycle can happen). I'll either close this or post an answer to my own question when I can.
See EDIT for a simpler repro that doesn't involve Subjects/Observables but is essentially the same problem.
I have a parent component that's responsible for fetching data from a service.
export class ParentComponent implements OnInit {
public mySubject: Subject<Foo[]> = new Subject<Foo[]>();
public buttonClicked = false;
private currentValues: Foo[] = null;
constructor(private SomeService myService) { }
this.myService.get().subscribe(values => {
this.mySubject.next(values); // Does NOT work when a child component is hidden, as expected.
this.currentValues = values; // Keep value so we can manually fire later.
});
private buttonClickHandler() {
this.buttonClicked = true;
this.mySubject.next(this.currentValues);
}
}
This data is subscribed to in the HTML by a child component. This component is hidden by default via *ngIf, and only becomes visible on a button click:
<app-child-component [values]="mySubject.asObservable()" *ngif="buttonClicked" />
In the parent component above you see I'm trying to pass the current available data to the child by invoking next() when the component is made visible in some way:
this.mySubject.next(this.currentValues);
This does not work when initially un-hiding the component via *ngIf. If I click the button a second time, which then calls next() again, then it works as expected. But when Angular is in the current context of un-hiding something, observables aren't getting their data. (This also happens when things are unhidden by other means, but the result is the same: If in the same method, the subject/data passing does not work; the component has to already be visible as of the method call.)
I'm guessing the binding to the observable is not happening until after *ngIf shows the child component, after the method call resolves. Is there some place I can hook into that I can then pass child data down?
EDIT for clarification: I don't believe this is an issue of Subject vs. BehaviorSubject. I'm not having issue passing the data. The issue is that the data-passing (confirmed via console.log()) is not occurring at all in the first place. It's not that the child component is receiving a null value. The subscription just isn't firing to the child.
I found I can reproduce this in a simpler fashion too: Trying to select an element in the DOM of *ngIf HTML reveals undefined if I make *ngIf's value true within the same Angular method.
<div *ngIf="buttonClicked">
<div id="someElement">Foo</div>
</div>
public someMethod(): void {
this.buttonClicked = true;
const container = document.getElementById('someElement'); // DOES NOT WORK if this.buttonClicked was false at the start of this method!
}
You going to need to use a BehaviourSubject instead of Subject, which emits the previously set value initially.
What is the difference between Subject and BehaviorSubject?
Inside my app.component , I have a background mode service which when shared via intent throws value to a behaviour Subject
this._notification.setNotiService2(data.extras);
Once logged In , I am setting the root to TabsPage
this.appCtrl.getRootNav().setRoot('TabsPage');
On Tabs Page , I have subscribed to the behaviour subject , so whenever I get a shared object , I process it and open a Modal displaying the required values.
Initally when the app opens , everything works fine. But once we login/logout the problem occurs. On logging out , I am setting the root page as Login Page.
this.appCtrl.getRootNav().setRoot('LoginPage');
Then again on successful login setting root to Tabs Page
this.appCtrl.getRootNav().setRoot('TabsPage');
Now again if I share the values via intent multiple instances of the Modal are opening with the exact same values.
I have checked for behaviour subject as being null/undefined but the subscribed value is Ok only . Logging the value from behaviour Subject inside the TabsPage , I found the same function (subscribed behaviour subject) is being called twice.
Again if I logout/login the Modal opens 3 times and the number continues to grow accordingly.
It sounds to me, that you are not remembering to unsubscribe, which means that the subscriptions increment each time. So whenever you leave a page, remember to unsubscribe to (all) your subscriptions. Since you are using Ionic, the ionViewWillLeave hook would be a suitable place to unsubscribe... so declare a new Subscription on your page and...
import { Subscription } from 'rxjs/Subscription';
// ...
mySubscription = new Subscription();
// ...
this.mySubscription = this.myService.mySubject.subscribe(....)
// ...
ionViewWillLeave() {
this.mySubscription.unsubscribe();
}
I currently have a router with a path set like so:
{path: "application/:id", component: ApplicationComponent}
I generate a menu of links by using an ngFor loop. it uses the index to generate a dynamic routerLink of:
routerLink="application/{{index}}"
Once clicked, it travels to the correct URL and uses my ApplicationComponent's ngOnInit method to grab the index in the url and pull a specific object out of an array in a separate class.
HOWEVER it only works for the first link you click on. If I click on link 1, it loads the page correctly with object 1's data. But when I click link 2, it travels to application/2 in the url but keeps object 1s info up. If i refresh the page and click link 2, object 2's info is shown, so I know it is pulling from the array correctly.
I suspect that once any links are clicked the ApplicationComponent uses the ngOnInit method to make the component, and then if another is clicked, this method is not called so the variables are not getting updated.
How can I solve this problem? I need the Application Component to know that ive traveled to a new sublink so it can call ngOnInit again
ngOnInit is only called once, when the component is loaded. So if you navigate to application/1, ngOnInit is called. If you then navigate to application/2, ngOnInit is not called again because the component is already loaded. You need to use something like paramMap from ActivatedRoute to detect when a parameter changes.
ngOnInit(){
this.activatedRoute.paramMap.subscribe(paramMap => {
let id = paramMap.get('id'); // id gets updated whenever parameters change
// add or call any code that needs to re-run when a parameter changes here
});
}
In Aurelia, I have a parent component that is composed of several other components. To keep this example simple, say that one component is a State dropdown, and another component is a City dropdown. There will be other components that depend on the selected city, but those two are enough to illustrate the issue. The parent view looks like this:
<template>
<compose view-model="StatePicker"></compose>
<compose view-model="CityPicker"></compose>
</template>
The idea is that you would pick a state from the StatePicker, which would then populate the CityPicker, and you would then pick a city which would populate other components. The routes would look like /:state/:city, with both being optional. If :state is omitted, then the url will automatically redirect to the first available state.
I'm using the EventAggregator to send messages between the components (the parent sends a message to the CityPicker when a state is selected). This is working, except for the initial load of the application. The parent is sending the message to the CityPicker, but the CityPicker component hasn't been activated yet, and it never receives the message.
Here's a Plunker that shows the problem. You can see that the city dropdown is initally empty, but it starts working once you change the state dropdown. Watch the console for logging messages.
So the question is: Is there a way to know that all the components are loaded before I start sending messages around? Is there a better technique that I should be using? Right now, the StatePicker sends a message to the parent that the state has changed, and then the parent sends a message to the CityPicker that the state has changed. That seems a little roundabout, but it's possible that the user could enter an invalid state in the url, and I liked the idea of being able to validate the state in one place (the parent) before all the various other components try to load data based on it.
The view/viewModel Pattern
You would want your custom elements to drive data in your viewModel (or in Angular / MVC language, controller). The viewModel captures information about the current state of the page. So for example, you could have a addressViewModel route that has state and city properties. Then, you could hook up your custom elements to drive data into those variables. Likewise, they could listen to information on those variables.
Here's an example of something you might write:
address.html
<state-picker stateList.one-way="stateList" value.bind="state" change.delegate="updateCities()"></state-picker>
<city-picker cityList.one-way="cityList" value.bind="city"></city-picker>
address.js
class AddressViewModel {
state = null;
city = null;
stateList = ['Alabama', 'Alaska', 'Some others', 'Wyoming'];
cityList = [];
updateCities() {
let state = this.state;
http.get(`API/cities?state=${state}`) // get http module through dependency injection
.then((response) => {
var cities = response.content;
this.cities.length = 0; // remove current entries
Array.prototype.push.apply(this.cities, cities);
});
}
}
If you wanted to get a little more advanced and isolate all of your state and city logic into their respective custom elements, you might try following this design pattern:
address.html
<state-picker value.bind="state" country="US"></state-picker>
<city-picker value.bind="city" state.bind="state"></city-picker>
address.js
class cityPickerViewModel {
#bindable
state = null;
cities = [];
constructor() {
// set up subscription that listens for state changes and calls the update
// cities function, see the aurelia documentation on the BindingEngine or this
// StackOverflow question:
// http://stackoverflow.com/questions/28419242/property-change-subscription-with-aurelia
}
updateCities() {
/// same as before
}
}
The EventAggregator Pattern
In this case, you would not want to use the EventAggregator. The EventAggregator is best used for collecting various messages from disparate places in one central location. For example, if you had a module that collected app notifications in one notification panel. In this case, the notification panel has no idea who might be sending messages to it, so it would just collect all messages of a particular type; likewise, any component could send messages whether or not there is a notification panel enabled.