I have a component in which I define a reactive variable, and I would like to use it in an imported function. A simple example of the code is available in the playground
App.vue
<script setup>
import { change } from './lib.js'
import { ref } from 'vue'
const msg = ref('one')
change()
</script>
<template>
<h1>{{ msg }}</h1>
</template>
lib.js
export const change = () => msg.value = 'two'
The code above of course does not work because change() does not know msg. Msg on the other hand is not exportable as a <setup script> only variable.
My question: how should I declare msg so that I can refer to it from that exported function? Or, alternatively, is it possible to declare a reactive variable in lib.js?
You could pass the msg as a param to your change method like this
lib.js
export const change = (msg) => msg.value = 'two'
and use it like that in your file
App.vue
const msg = ref('one')
change(msg)
Related
I know that you are unable to dynamically generate an arbitrary string in TailwindCSS like hue-rotate-[${randomHueColor}deg], because the arbitrary string has to exist at build time.
I see that it also seems impossible to generate the string in a different component and pass it through a prop to the component.
Eg.
<script setup>
import {ref, onBeforeMount} from 'vue'
import ImageComponent from './components/ImageComponent.vue'
const randomHue = ref('')
function generateRandomHue(){
let random = Math.floor(Math.random() * 361);
randomHue.value = String(`hue-rotate-[${random}deg]`) // or String(`filter: hue-rotate(${random}deg)`)
}
onBeforeMount(() => {
generateRandomHue()
})
</setup>
<template>
<ImageComponent :hueColor="randomHue" />
</template>
On the component side, I've tried both class: and style: (with filter:).
Is there another way to go about this, so I can have a truly dynamic random arbitrary hue-rotate?
When a Vue template ref is mounted, I want to get the nearest parent Vue component. This should be generic and work for any template ref so I've put it in a composition function (but that's just an implementation detail).
I had this working but my implementation used elem.__vueParentComponent while iteratively searching an element's ancestors. While reading the Vue source code I saw __vueParentComponent was only enabled for dev mode or if dev tools is enabled in production. Thus, I don't want to rely on that flag being enabled.
I thought this might be possible using vnodes but this isn't easily google-able. Here's an example of what I'm trying to do:
function useNearestParentInstance(templateRef) {
function getNearestParentInstance(el) {
// code here
}
onMounted(() => {
const el = templateRef.value;
const instance = getNearestParentInstance(el);
// do something with instance
});
}
<template>
<div>
<SomeComponent>
<div>
<div ref="myElem"></div>
</div>
</SomeComponent>
</div>
</template>
<script>
export default {
setup() {
const myElem = ref();
// nearest would be SomeComponent's instance in this case
useNearestParentInstance(myElem);
...
}
}
</script>
If you want the nearest vue parent you can simply use
ref().$parent // Not sure if syntax is same in vue3
ref().$parent will get the first vuecomponent that is the parent of the ref that you placed.
Vue documentation gives an example of simple state management, for a single-file app:
const sourceOfTruth = {}
const vmA = new Vue({
data: sourceOfTruth
})
const vmB = new Vue({
data: sourceOfTruth
})
How to use the same mechanism for components?
I tried to move this concept of a minimal state manager to components in a codesandbox.io sandbox. It did not work and the more meaningful error, I believe, is
The "data" option should be a function that returns a per-instance value in component definitions.
Does this mean that components must be completely standalone and cannot rely on data managed outside of them?
try this Sandbox updated
in dataMaster.js
var store = {
state: {
message: "Hello!"
}
};
module.exports = store;
and component.vue
<template>
<div>
<h1>{{sharedState}}</h1>
</div>
</template>
<script>
import store from "./dataMaster";
export default {
name: "HelloWorld",
data() {
return {
privateState: {},
sharedState: store.state
};
}
};
</script>
The "data" option should be a function that returns a per-instance value in component definitions
It means that the data property you define in the component must be a function, and that function should return a per-instance value. You should, not any must here, and it can be tricked.
About the error, you got the error not because of state management, but because you forgot to export the function in dataMaster.js, so you couldn't import and use it in HelloWorld.vue. You got the error because you didn't return a function, that was the function part, not per-instance or anything related to state management.
To do the trick that I think you want, here it is: https://codesandbox.io/s/unruffled-carson-l3oui. You change the same source of truth directly from components, yet without tools like VueX or something. But it's tricky and is exactly what the error try to avoid, return a per-instance value. I don't know what the advantages and disadvantages of it yet, but at the end of the day, I think to do state management, we should use the standard recommended way from the prior people, like VueX, etc, just choose one from tons of them.
// dataMaster.js
const data = {
msg: "hello From dataMaster"
};
export default function dataMaster() {
return data // this is not "per-instance", they're the same across all instances
}
//Hello.vue
<template>
<div>
{{msg}}
<button #click="change">Change</button>
</div>
</template>
<script>
import dataMaster from "./dataMaster.js";
export default {
name: "HelloWorld",
data: dataMaster,
methods: {
change() {
this.msg = this.msg === "message1" ? "message2" : "message1";
}
}
};
</script>
"data" option should be a function, cause pure object could make data mess up. e.g when "sourceOfTruth" modified by componentA but you are focus on componentB, and you will confused. so please use "vuex" or "eventbus".
If you want to manage all data from global state. you should search and learn Vuex and Store Management.
Maybe using "Event Bus" is better for you example.
And you're wrong with this code, we always need to export the code.
export default function dataMaster() {
return {
msg: "hello from dataMaster"
};
}
I have this code:
import Vue from 'vue'
import s from 'vue-styled-components'
import Test1x from './test1x'
export default Vue.extend({
name:'test1',
render(){
const Div=s.div`
`
const test1x1=new Test1x()
const test1x2=new Test1x()
const el=
<Div>
{test1x1.state.greeting}
{test1x2.state.greeting}
<button vOn:click={()=>test1x1.commit('change')}>change</button>
<button vOn:click={()=>test1x2.commit('change')}>change</button>
</Div>
return el
}
})
and test1x.js file is as follows:
import withStore from './withStore'
export default withStore({
state: {
greeting:'hola'
},
mutations: {
change(state){state.greeting='hello'}
}
})
and withStore.js file is as follows:
import Vue from 'vue'
export default ({ state, mutations }) => {
return Vue.extend({
data () {
return { state }
},
methods: {
commit (mutationName) {
mutations[mutationName](this.state)
},
},
})
}
Given that code, I assume each greeting will be changed by the corresponding button, separately, individually, but not, when I press a button all two greetings change. Anyone knows why? Thank you in advance.
And even more strange is that while at least code presented before is reactive, I mean, greeting change when pressing a button, code below it is not:
import Vue from 'vue'
import s from 'vue-styled-components'
import withStore from './withStore'
export default Vue.extend({
name:'test1',
render(){
const Div=s.div`
`
const Test1x=withStore({
state: {
greeting:'hola'
},
mutations: {
change(state){
state.greeting='hello'
}
}
})
const test1x1=new Test1x()
const test1x2=new Test1x()
const el=
<Div>
{test1x1.state.greeting}
{test1x2.state.greeting}
<button vOn:click={()=>test1x1.commit('change')}>change</button>
<button vOn:click={()=>test1x2.commit('change')}>change</button>
</Div>
return el
}
})
when pressing button nothing happens, greeting remains with hola instead of hello. Isn't that strange? Anyone knows why? Thanks again.
edit
thanks to #skirtle answer, I solved the issue doing this:
import Vue from 'vue'
import s from 'vue-styled-components'
import Test1 from './test1/test1'
import Test1x from './test1/test1x'
export default Vue.extend({
name:'app',
render(){
const Div=s.div`
`
const test1x1=new Test1x()
const test1x2=new Test1x()
//test1x1.commit('init')
test1x1.state={greeting:'hola'}
test1x2.state={greeting:'hola'}
console.log(test1x1.state)
const el=
<Div>
<Test1 test1x={test1x1}/>
<Test1 test1x={test1x2}/>
</Div>
return el
}
})
and test1.js being this:
import Vue from 'vue'
import s from 'vue-styled-components'
export default Vue.extend({
props:{
test1x:Object
},
name:'test1',
render(){
const Div=s.div`
`
const el=
<Div>
{this.test1x.state.greeting}
<button vOn:click={()=>this.test1x.commit('change')}>changes</button>
</Div>
return el
}
})
and test1x.js being this:
import withStore from './withStore'
export default withStore({
state: null,
mutations: {
change(state){state.greeting='hello'},
init(s){s={greeting:'hola'}
console.log(s)}
}
})
This works. The strange thing now is that if I uncomment test1x1.commit('init') I get an infinite loop, don't know why. If I then comment test1x1.state={greeting:'hola'} I don't get an infinite loop but I get an error that cannot read property greeting of null in test1.js. Anyone knows why this is happening? The thing is test1x1.commit('init') does not change the value test1x1.state, it remains null. Thanks.
Addressing the first problem first.
The problem starts here:
state: {
greeting:'hola'
},
The value of state points to a specific object. That object then gets passed around but at no point is a copy taken. The result is that both test1x1 and test1x2 will have the same object for state.
You can confirm this by adding a bit of console logging:
console.log(test1x1.state === test1x2.state)
The way Vuex handles this problem is to allow state to be a function, just like data:
state () {
return {
greeting:'hola'
}
},
Each time the state function is invoked it will return a new object.
As you aren't using Vuex you would need to ensure that you call the state function at the correct point to generate the relevant object. Something like this:
data () {
if (typeof state === 'function') {
state = state()
}
return { state }
},
So, to your second problem. I'm afraid I don't know what the problem is there. However, I very much doubt that 'when pressing button nothing happens'. It may not update the message but that isn't the same as 'nothing happens'. It should be relatively straightforward to add in some console logging at each stage and to establish exactly what does and doesn't happen. Once you've gathered all of that extra information about precisely what is happening it should be fairly simple to pinpoint precisely where the disconnect is occurring.
My suspicion would be that you've made some other changes to withStore that are causing this new problem. It could also be a file caching problem, so that the code you're running is not the code you think it is. Either way the extra logging should reveal all.
If you need further help with that then please update the question with the extra information gathered via console logging.
Update:
This is why the updated code causes an infinite rendering loop:
Inside the render function there is a call to test1x1.commit('init').
Inside commit it accesses the property this.state. This will add the property this.state as a rendering dependency for the component. It doesn't matter what the current value of this.state is, it's the property itself that is the dependency, not its current value.
On the next line it sets test1x1.state={greeting:'hola'}. This changes the value of the state property. This is the same state that has just been registered as a rendering dependency. As a rendering dependency has now changed the component will be re-added to the rendering queue, even though it hasn't finished the current rendering yet.
Eventually Vue will work its way through the rendering queue and get back to this same component. It will again call render to try to render the component. The previous steps will all occur again and so the component keeps being rendered over and over.
The bottom line here is that you shouldn't be initialising these data structures within the render function in the first place. There are various places you might create them but inside render does not appear to be appropriate based on the code you've provided.
When working with vue-class-component, I need to declare my props both in the #Component decorator AND in the class itself. It seems rather redundant and error prone.
Is this intended or am I missing something here?
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
// Here we declare that score and counter are props
#Component({
props: {
score:Number,
counter:Number
}
})
// Now I have to declare score and counter again ?
// Adding to the confusion is the fact that Typescript types are
// written differently from Vue types (number vs Number)
export default class ScoreBar extends Vue {
score:number
counter:number
created(){
console.log(`score prop is ${this.score} counter is ${this.counter}`)
}
}
</script>