How could I use mobx in nodejs but not browser? - mobx

I try to use mobx in nodejs but the script is not work.
(async function () {
const mobx = require('./mobx.umd.min.js')
// example 2, array of primitives
// observes computed value, works
const { observable, computed, autorun } = mobx;
var numbers = observable([1, 2, 3]);
autorun(() => {
console.log(numbers);
});
numbers.push(4); //autorun does not trigger
numbers.push(4); //autorun does not trigger
})();
but the script works in broswer:
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mobx/5.15.4/mobx.umd.min.js"></script>
<script type="module">
// example 2, array of primitives
// observes computed value, works
const { observable, computed, autorun } = mobx;
var numbers = observable([1, 2, 3]);
autorun(() => {
window.numbers = numbers;
const app = document.querySelector("#app");
console.log(`numbers`, numbers);
app.innerHTML = "<p>" + numbers.join(", ") + "</p>";
});
numbers.push(4); //autorun does not trigger
</script>
</body>
</html>
I want to use mobx in nodejs, How could I use mobx in nodejs directly?

You are referencing numbers array inside Node.js autorun, but you have not changed it, so there is not reason to MobX to autorun. Observers (e.g. autorun, computed, observer and etc.) only react when something was changed.
You only changed array content, not the array itself. So if your code was something like that:
autorun(() => {
console.log(numbers.map(x => x);
});
It would work as expected, because here you actually referencing array content. And browser version works because you are using numbers.join and referencing array content with that call.
Next time try to test identical cases in both environments just to be sure that the error in not in your code!

Related

Why does watchEffect trigger while the watched value is not changed?

I have a component which keeps a local partial copy of an Pinia storage data.
<template>
<h3>Order idx {{ idx }}: sum = {{ localSum }}, store sum = {{ getOrderSum() }}</h3>
<input type="number" v-model="localSum" />
<button #click="updateSum">Save</button>
</template>
<script setup>
import { useCounterStore } from '../store/counter'
import { watchEffect, ref, defineProps, watch } from 'vue'
const props = defineProps({
idx: 0
})
const store = useCounterStore()
const localSum = ref(0)
function getOrderSum() {
return store.getOrderIdxSumMap[props.idx]
}
function updateSum() {
store.setOrderSum(props.idx, localSum.value)
}
watch(
() => getOrderSum(),
(newValue) => {
console.log('i am updated')
localSum.value = newValue
}, {
immediate: true,
}
)
/*
watchEffect(() => {
console.log('i am updated')
localSum.value = getOrderSum()
})
*/
</script>
Whenever external data changes the local copy should update. Using watchEffect instead of watch causes components with modified and unsaved data to lose user input.
watchEffect behaviour description 1:
Change first order data
Click save
You'll see i am updated twice within console.
watchEffect behaviour description 2:
Change the first order data
Change the second order data
Click save on the first order
You'll see the second order changes lost
Comment out watchEffect and uncomment watch. Now everything works just fine. Is it my misconseptions or a bug worth to be reported?
Full demo
It is how watch and watchEffectsupposed to work
In short, watch and watchEffect both tracks the change of their dependencies. When the dependencies changed they act like follow:
watch recalculates its source value and then compares the oldValue with newValue. If oldValue !== newValue, it will trigger the callback.
watchEffect has no sources so it triggers the callback anyway
In your example the dependencies of both watch and watchEffect are store.getOrderIdxSumMap and props.idx. The source of watch is the function () => getOrderSum(). So when the store.getOrderIdxSumMap changed, watchEffect will always trigger the callback but watch will re-calculate the value of () => getOrderSum(). If it changed too, watch will trigger its callback
When Vue detects a dependency is changed?
Whenever you set a value for reactive data, Vue uses a function to decide if the value is changed as follows:
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
So setting the same value for a primitive type (number, string, boolean...) will not be considered as a value change

How can I reset the value of a ref and keep an associated watcher working?

UPDATE:
I have achieved the desired behavior in the MCV by changing resetArray:
function resetArray() {
// myArray.value = [] // old version
myArray.value.length = 0 // new version
}
But I still don't understand why my MCV doesn't work.
ORIGINAL POST:
Background
In an app I am building, I store data in an a ref, created as const myArray = ref([]), which takes the form of an array of objects. This array is only changed in the following ways:
myArray.value[index] = {key: value}
myArray.value = [].
In particular, at no time is an object in myArray modified, it is either created or replaced.
I later added a watch which took action on every change to myArray.value. I discovered that after resetting myArray to [], the watcher stopped getting called.
Things I have tried:
I confirmed that my usage of ref follows the guidelines in this SO answer regarding ref vs reactive.
Refactoring to use watchEffect instead of watch. Did not help.
Refactoring to use reactive rather than ref. Did not help.
My Issue
In the MCV below, modifying myArray by calling addToArray works as intended: myArray.length is rendered and the first watch is triggered.
Calling resetArray triggers only the second watch, but the first watch IS NOT triggered when addToArray is called afterwards.
My Question
How can I both keep the ability to set myArray to [] and trigger actions every time myArray changes?
My MCV
View my MCV on Vue SFC Playground
The below code is the content of App.vue in a Vue project created with npm init vue#latest:
<script setup>
import {ref, watch} from "vue"
const myArray = ref([])
function addToArray() {
myArray.value.push("1")
}
function resetArray() {
myArray.value = []
}
watch(myArray.value, () => {
console.log("CLICKED!")
})
watch(myArray, () => {
console.log("RESET! clicked won't get called again!")
})
</script>
<template>
{{myArray.length}}<br />
<button #click="addToArray">CLICK ME</button><br />
<button #click="resetArray">RESET</button>
</template>
When watching a ref, use the ref itself -- not its value property -- as the watch source (the 1st argument to watch()).
To observe new array assignments or item additions/removals, pass the deep:true option (the 3rd argument to watch()):
watch(
myArray 1️⃣,
() => { /* handle change */ },
{ deep: true } 2️⃣
)
demo

Why would a Vue3 watcher of a prop not be triggered? (Composition API)

I am desperately trying to watch a prop in Vue3 (3.2.31, Composition API):
<script lang="ts" setup>
import { toRef, watch } from 'vue'
const props = defineProps({
'trigger-refresh': {
type: String
}
})
const triggerRefresh = toRef(props, 'trigger-refresh')
watch(triggerRefresh, (a, b) => console.log('props triggered refresh'))
</script>
I trigger the emission of trigger-refresh, it is passed to the component with the code above and I see triggerRefresh changing in DevTools→Vue.
So everything is fine up to the inside of the component except that the watch is not triggered (i.e. there is no message on the console).
I read the documentation for Watchers and responses to a question about watching props (one of the responses is almost identical to what I did) but I simply fail to understand why this does not work in my case.
Try to add the immediate option in order to trigger the watch at the first prop change:
watch(triggerRefresh, (a, b) => console.log('props triggered refresh'), {
immediate: true
})
Prop names are normalized to camel case. As it was stated in the question, it is triggerRefresh and not trigger-refresh that is seen updated. So it should be:
const triggerRefresh = toRef(props, 'triggerRefresh')
watch(triggerRefresh, ...)
Or just:
watch(() => props.triggerRefresh, ...)

Vue test utils - setChecked() not updating v-model

I am writing unit tests for some components I made at my job. We are using Mocha (TDD) and the Chai assertion library. I have a component with some checkboxes, and using the setChecked() method on them from vue-test-utils is not behaving as expected. I have made a small example that reproduces the error:
TestComponent.vue:
<template>
<div>
<input class="checkboxTest" type="checkbox" v-model="cbVal">
<input class="inputTest" type="text" v-model="textVal">
</div>
</template>
<script>
define([], function() {
return {
data: function() {
return {
cbVal: false,
textVal: ""
}
}
}
})
</script>
test.js:
suite("Random test", function() {
var VueTest;
var TestComponent;
//Import the vue test utils library and TestComponent
suiteSetup(function(done) {
requirejs(
["vue-test-utils", "vuec!components/TestComponent"],
function(VT, TC) {
VueTest = VT;
TestComponent = TC;
done();
}
);
});
//This test passes
test("fill in the input", function() {
var wrapper = VueTest.mount(TestComponent);
wrapper.find(".inputTest").setValue("Hello, world!");
assert.equal(wrapper.vm.textVal, "Hello, world!");
});
//This one does not
test("programatically check the box", function() {
var wrapper = VueTest.mount(TestComponent);
wrapper.find(".checkboxTest").setChecked(true);
//Prints out AssertionError: expected false to equal true
assert.equal(wrapper.vm.cbVal, true);
});
});
The textVal data member in TestComponent is getting changed, but cbVal is not. Can anyone please explain why setValue() works just fine, but setChecked() does not? Thank you in advance.
I had a similar issue and the accepted answer did not solve my problem. I don't think the accepted answer is correct either, as setChecked was added specifically to avoid having to manually set the values via the elements.
In my case, I wanted Vue to react to the v-model change and redraw. I tried async and many other methods, until finding the one that works: wrapper.vm.$forceUpdate().
Here's what my working code looks like:
wrapper.find("#someRadioButtonId").setChecked(true)
// manually force Vue to update
wrapper.vm.$forceUpdate()
expect(wrapper.find("#someRadioButtonId").classes()).toContain("selected") // success!
I can't answer why it doesn't work, but I can tell you your approach is incorrect in the first place.
You shouldn't be interacting with the html elements directly to set their values. When you set vue-model to cbVal you should instead be interacting with cbVal.
In other words, change your code from setChecked() to cbVal = true in order for it to comply with how Vue wants you to develop your project. There's no guarantee Vue can remain dynamic and reactive if you don't interact with your code the way Vue wants you to.

How to access props data?

I have an PHP var used in a blade template and want to pass it to a vue's method.
I'm still learning so sorry if it seems obvious but I read the docs but found noting useful.
So I have this piece of code in my HTML
<chat-messages :messages="messages" :surgery_id="{{ $surgery->id }}"></chat-messages>
And in my JS
Vue.component('chat-messages', require('./components/ChatMessages.vue'));
const app = new Vue({
el: '#chat',
methods: {
fetchMessages() {
axios.get('/messages/').then(response => {
this.messages = response.data;
});
},
}
});
And I want to use something like axios.get('/messages/' + surgery_id).then(...)
But I can't figure out how to retrieve this surgery_id variable
In my ChatMessages.vue, I well created the properties
<template>
//Stuff to loop & display
</template>
<script>
export default {
props: ['messages' , 'surgery_id']
};
</script>
Use this as you do normally with the data:
axios.get('/messages/' + this.surgery_id).then(...)
You can access all the property of data option,props, and methods using this as context.
Further, if you want to use ES6, then it's even easier without concatenating them: (using tilde key `)
axios.get(`/messages/${this.surgery_id}`).then(...)
As per your query, you also need to pass props in your instance:
const app = new Vue({
// ...
propsData:{
surgery_id: 'your id value'
}
See my another post for more help.