How are you. I am newbie of Rxjs. I am not sure how to merge observable from different event. I integrated Rxjs with Vue.js
export default {
name: 'useraside',
data: function () {
return {
searchKey: '',
isPublic: true
}
},
components: {
User
},
subscriptions () {
return {
// this is the example in RxJS's readme.
raps: this.$watchAsObservable('searchKey')
.pluck('newValue')
// .filter(text => text.length > 1)
.debounceTime(500)
.distinctUntilChanged()
.switchMap(terms => fetchRaps(terms, this.userdata._id, this.isPublic))
.map(formatResult)
}
}
}
Now event comes from searchKey changes, now I would like to subscribe same observable when isPublic value change.
So I would like to get raps whenever searchKey changes or isPublic changes.
Thanks.
You could use the merge operator and keep using the this.isPublic in your switchMap, as Maxime suggested in the comment.
But I'd rather go with a nice a pure dataflow where you listen for the two values and consume them in your handlers. Something like
Rx.Observable.combineLatest(
this.$watchAsObservable('searchKey').pluck('newValue'),
this.$watchAsObservable('isPublic').pluch('newValue'),
([searchKey, isPublic]) => ({ searchKey, isPublic })
)
.dedounceTime(500)
.distinctUntilChanged()
.switchMap(({ searchTerm, isPublic }) => fetchRaps(searchTerm, this.userdata._id, isPublic))
Or event better is you can change the initial data structure to something like :
data: function () {
return {
searchConfig: {
searchKey: '',
isPublic: true
}
}
},
you can then remove the combineLatest and only watch the searchConfig property.
The benefit of this implementation is that your dataflow is pure and doesn't depend on any external context (no need for the this.isPublic). Every dependency is explicitly declared at the beginning of the dataflow.
If you want to go even further, you can also watch the userdata and explicitly pass it down the dataflow :)
Related
Context:
I have a reports application that contains a report editor. This Report Editor is used to edit the contents of the report, such as the title, the criteria for filtering the results, the time range of results, etc..
The Problem:
There is something wrong with the way I have used Vuex/Vuejs in my components I believe. My store contains getters for each aspect of this report editor. Like this:
const getters = {
activeReportTitle: state => {
return state.activeReport.title;
},
activeReportID: state => {
return state.activeReport.id;
},
timeframe: state => {
return state.activeReport.timeframe;
},
includePreviousData: state => {
return state.activeReport.includePreviousData;
},
reportCriteria: state => {
return state.activeReport.reportCriteria;
},
emailableList: state => {
return state.activeReport.emailableList;
},
dataPoints: state => {
return state.activeReport.configuration?.dataPoints;
},
...
Each getter is used in a separate component. This component uses the getter only to initialize the local data, and uses actions to modify the state. The way I have done this is by adding a local data property and a watcher on the getter that changes the local data property. The component is using the local data property and that data property is sent to the action and the getter is updated.
ReportSearchCriteria.vue
...
data() {
return {
localReportCriteria: [],
currentCriteria: "",
};
},
watch: {
reportCriteria: {
immediate: true,
handler(val) {
this.localReportCriteria = [...val];
}
}
},
computed:{
...reportStore.mapGetters(['reportCriteria'])
},
methods: {
...reportStore.mapActions(["updateReportCriteria"]),
addSearchCriteria() {
if (this.currentCriteria) {
this.localReportCriteria.push(this.currentCriteria);
this.updateReportCqriteria(this.localReportCriteria);
}
this.currentCriteria = "";
this.$refs['reportCriteriaField'].reset();
},
...
The hierarchy of the components is set up like this
Reports.Vue
GraphEditor.vue
ReportSearchCriteria.vue
Could you clarify what the problem is? Does the 'reportCriteria' not get updated when it's supposed to? How does the function 'updatedReportCriteria' look like? You use mutations to update a state in the store. Also, you have a typo when you're calling the action.
I have an object that I am watching in vue for the purpose of performing an action whenever a change is detected in it. Something keeps triggering it, but when I print the object to the console and compare the oldVal to newVal they seem identical.
Just looking at the objects logged to the console revealed no differences to my eye, so I thought that by stringifying them and comparing them in a text compare tool I would find differences, but there too the results were identical for code like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
console.log('CC changed: ', JSON.stringify(newVal), ' | was: ', JSON.stringify(oldVal))
}
},
While not understanding why the watch was being triggered if nothing in the object had changed, I thought it was safe to do something like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (newVal !== oldVal) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But the log ran, despite there being no discernible difference I could find!
So I found a working solution by doing this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But this still leaves me the nagging question of WHY this is being triggered in the first place. What could possibly be changing and why?
Supplementary info
CCompPrefs is coming via a computed element in the following way:
computed: {
CCompPrefs () {
return this.$store.state[this.$attrs.useCase].filter(x => (x.show === true && x.enabled === true))
},
}
Almost any action will seemingly trigger this watch. Like throwing up a model window.
Using Vue devtools, I can verify that there are NO mutations being applied to ANY part of the vuex store
UPDATE
Now I am wondering if this.$attrs.useCase in my computed value above might be the culprit. The modal I am opening is in a parent container, perhaps that switches the context for that value and forces an update? Looking into it now...
UPDATE2
Nope. this.$attrs.useCase does NOT change. So still confused, WHAT could be triggering this watcher?
I avoided redundant calls for unchanged data by crudely checking the object matches in my handler like this:
data: () => ({
lastDataString: '',
}),
itinerary: {
handler: function(v) {
// Avoid redundant calls
let dataString = JSON.stringify(v)
if (dataString === this.lastDataString){
return
}
this.lastDataString = dataString
// do stuff
},
deep: true,
},
I couldn't find the answer anywhere.
Let's say we have Vuex store with the following data:
Vuex store
state: {
dialogs: {
dialogName1: {
value: false,
data: {
fileName: '',
isValid: false,
error: '',
... 10 more properties
}
},
dialogName2: {
value: false,
data: {
type: '',
isValid: false,
error: '',
... 10 more properties
}
}
}
}
Dialogs.vue
<div v-if="dialogName1Value">
<input
v-model="dialogName1DataFileName"
:error="dialogName1DataIsValid"
:error-text="dialogName1DataError"
>
<v-btn #click="dialogName1Value = false">
close dialog
</v-btn>
</div>
<!-- the other dialogs here -->
Question
Let's say we need to modify some of these properties in Dialogs.vue.
What's the best practices for creating a getter and setter for every dialog property efficiently, without having to do it all manually like this:
computed: {
dialogName1Value: {
get () {
return this.$store.state.dialogs.dialogName1.value
},
set (value) {
this.$store.commit('SET', { key: 'dialogs.dialogName1.value', value: value })
}
},
dialogName1DataFileName: {
get () {
return this.$store.state.dialogs.dialogName1.data.fileName
},
set (value) {
this.$store.commit('SET', { key: 'dialogs.dialogName1.data.fileName', value: value })
}
},
dialogName1DataIsValid: {
get () {
return this.$store.state.dialogs.dialogName1.data.isValid
},
set (value) {
this.$store.commit('SET', { key: 'dialogs.dialogName1.data.isValid', value: value })
}
},
dialogName1DataIsError: {
get () {
return this.$store.state.dialogs.dialogName1.data.error
},
set (value) {
this.$store.commit('SET', { key: 'dialogs.dialogName1.data.error', value: value })
}
},
... 10 more properties
And this is only 4 properties...
I suppose I could generate those computed properties programmatically in created(), but is that really the proper way to do it?
Are there obvious, commonly known solutions for this issue that I'm not aware of?
getters can be made to take a parameter as an argument - this can be the 'part' of the underlying state you want to return. This is known as Method-style access. For example:
getFilename: (state) => (dialogName) => {
return state.dialogs[dialogName].data.fileName
}
You can then call this getter as:
store.getters.getFilename('dialogName1')
Note that method style access doesn't provide the 'computed property' style caching that you get with property-style access.
For setting those things in only one central function you can use something like this:
<input
:value="dialogName1DataFileName"
#input="update_inputs($event, 'fileName')">
// ...
methods:{
update_inputs($event, whichProperty){
this.$store.commit("SET_PROPERTIES", {newVal: $event.target.value, which:"whichProperty"})
}
}
mutation handler:
// ..
mutations:{
SET_PROPERTIES(state, payload){
state.dialogName1.data[payload.which] = payload.newVal
}
}
Let me explain more what we done above. First we change to v-model type to :value and #input base. Basically you can think, :value is getter and #input is setter for that property. Then we didn't commit in first place, we calling update_inputs function to commit because we should determine which inner property we will commit, so then we did send this data as a method parameter (for example above code is 'fileName') then, we commit this changes with new value of data and info for which property will change. You can make this logic into your whole code blocks and it will solved your problem.
And one more, if you want to learn more about this article will help you more:
https://pekcan.dev/v-model-using-vuex/
I am using a simple state manager (NOT vuex) as detailed in the official docs. Simplified, it looks like this:
export const stateholder = {
state: {
teams: [{id: 1, name:'Dallas Cowboys'}, {id: 2, name:'Chicago Bears'}, {id: 3, name:'Philadelphia Eagles'}, {id:4, name:'L.A. Rams'}],
selectedTeam: 2,
players: []
}
getPlayerList: async function() {
await axios.get(`http://www.someapi.com/api/teams/${selectedTeam}/players`)
.then((response) => {
this.state.players = response.data;
})
}
}
How can I (reactively, not via the onChange event of an HTML element) ensure players gets updated (via getPlayerList) every time the selectedTeam changes?
Any examples of simple state that goes a little further than the official docs? Thank you.
Internally, Vue uses Object.defineProperty to convert properties to getter/setter pairs to make them reactive. This is mentioned in the docs at https://v2.vuejs.org/v2/guide/reactivity.html#How-Changes-Are-Tracked:
When you pass a plain JavaScript object to a Vue instance as its data
option, Vue will walk through all of its properties and convert them
to getter/setters using Object.defineProperty.
You can see how this is set up in the Vue source code here: https://github.com/vuejs/vue/blob/79cabadeace0e01fb63aa9f220f41193c0ca93af/src/core/observer/index.js#L134.
You could do the same to trigger getPlayerList when selectedTeam changes:
function defineReactive(obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val;
},
set: function reactiveSetter(newVal) {
val = newVal;
stateholder.getPlayerList();
}
})
}
defineReactive(stateholder.state, 'selectedTeam');
Or you could set it up implicitly using an internal property:
const stateholder = {
state: {
teams: [/* ... */],
_selectedTeam: 2,
get selectedTeam() {
return this._selectedTeam;
},
set selectedTeam(val) {
this._selectedTeam = val;
stateholder.getPlayerList();
},
players: []
},
getPlayerList: async function() {
/* ... */
},
};
Your question is also similar to Call a function when a property gets set on an object, and you may find some more information there.
You could use v-on:change or #change for short to trigger getPlayerList.
Here a fiddle, simulating the request with setTimeout.
As the title says, I'm trying to change the value of a prop/data in a component, but the trigger is being fired from outside the component, from something that has nothing to do with Vuejs.
Currently I trying to use a Simple State Manager, based on the docs from here, like so:
var store = {
debug: true,
state: {
progress: 23
},
setProgress (uff) {
if (this.debug) console.log(uff)
this.state.progress = uff
}
}
The documentation leads me to believe that if the value of progress is mutated, the value of my Vue instance would also change if I link them accordingly. But this doesn't work in a component (my guess would be it's cause it's a function).
This is part of my component:
Vue.component('transcoding', {
data () {
return {
progress: store.state.progress
}
},
template: `
<v-progress-circular
:size="130"
:width="25"
:value="progress"
color="teal"
>
{{progress}}
</v-progress-circular>
`
})
So, when I trigger a store.setProgress(value), nothing happens. The log messages do happen, but the state isn't updated in the component.
This is the script that's currently triggering the state change:
App.cable.subscriptions.create(
{ channel: "ProgressChannel", room: "2" },
{ received: function() {
store.setProgress(arguments[0])
}}
)
It's an ActionCable websocket in Ruby on Rails. The trigger works perfectly, but I just cannot make the connection between the state change and the component.
I tried loading this script in the mounted() event for the component, thinking I could reference the value like this:
Vue.component('transcoding', {
data () {
return {
progress: 0
}
},
template: `
<v-progress-circular
:size="130"
:width="25"
:value="progress"
color="teal"
>
{{progress}}
</v-progress-circular>
`,
methods: {
setProgress: function(uff) {
this.progress = uff
}
},
mounted() {
App.cable.subscriptions.create(
{ channel: "ProgressChannel", room: "2" },
{ received: function() {
this.setProgress(arguments[0])
}}
)
}
})
But this gives me an error saying that this.setProgress is not a function, which is obvious since I'm calling it within the create method of App.cable.subscriptions.
How can I make this work? I realize I'm mixing things with my question, but I wanted to illustrate what my goal is. I simply want to know how to make the component's progress data to update, either from the outside, or from the component itself if I can make it find the function.
You are initializing your data item to the value from the store:
data () {
return {
progress: store.state.progress
}
}
Changes to the store will not propagate to your data item. You could eliminate the data item and just use store.state.progress where you need it, or you could create an computed that returns its value if you want a local single-name handle for it.