Please take a look at this not-working pseudo code:
Vue.component('child', {
props: [],
template: '<div><input v-model="text"></div>',
data: function() {
return {child-text: ""}
}
})
Vue.component('parent', {
template: '<h1> {{text}} </h1>'
data: function() {
return {parent-text: ""}
}
})
What is the most elegant way to fix this code that whenever the user changes the content of input box in child component, then the variable child-text in child component and the variable parent-text in parent component will change automatically? I also want that if the variable child-text and/or parent-text change then the content of input box will change respectively?
I solved this with my own little data store, its a very simple approach but works good enough for me without the necessity to dive into Vuex.
First, I create my data store somewhere before initializing anything else.
window.globalData = new Vue({
data: {
$store: {}
},
});
After that, I add a global Mixin that allows to get and set data to the global storage.
Vue.mixin({
computed: {
$store: {
get: function () { return window.globalData.$data.$store },
set: function (newData) { window.globalData.$data.$store = newData; }
}
}
});
Then, every component can access the data storage by this.$store. You can check a working example here:
https://codesandbox.io/s/62wvro7083
Related
I have a Vuex store where I have a getter which works correctly and I can see the changes on the state. But if I call this getter as computed property in component it does not work. The value is still the same.
The store code looks like:
mutations: {
UPDATE_SERVER_FILTERS(state, payload) {
this._vm.$set(state, 'serverFilters', payload);
//state.serverFilters = payload; // Both patterns work
},
getters: {
serverFilters(state) {
return state.serverFilters; // This works fine
}
}
}
The component code:
computed: {
serverFilters() {
return this.$store.getters[this.storeName + '/serverFilters'];
},
}
Here is JSFiddle example https://jsfiddle.net/camo/je0gw9t3/4/ which works fine. And it is a problem cause in my project it does not work. I am prepared to die...
How can I solve it?
In the most bottom part:
new Vue({
store,
el: '#example',
data() {
return {};
},
computed: {},
methods: {
changeFilters() {
this.$store.dispatch(this.storeName + '/updateFilters');
// ^^^^^^^^^^^^^^ there is no storeName
},
},
});
The changeFilters method. You are using this.storeName, but there is no this.storeName! Just like the Child component, add storeName: 'a' to the data() then it should work.
https://jsfiddle.net/4yfv3w87/
Here is the debug process for your reference:
First open the Vue Devtools and switch to the timeline tab. And just click the button, you will see that there is no action is being fired. So the problem must be the one who dispatches the action. And then you will notice that the root component doesn't have a storeName.
So don't panic, just try to trace the code. It will only take a few minutes to find out the issue!
Computed properties might have problem to make an observer reference from returned value out of function. Instead of chaining getters and computed properties, why you don't use just getters or computed properties ? In my opinion, it's a bad practice to use them both, and I can't imagine a situation you need it. So if you need filter operations in many components, just make a getter and use getter in components instead of computed properties.
If you really want to chain them, try this:
new Vue({
store,
el: '#example',
data() {
return {
storeName: 'a'
}
},
computed: {
filters() {
get() {
return this.$store.getters[`${this.storeName}/getFilters`];
}
set(newValue) {
this.$store.dispatch(this.storeName + '/updateFilters');
}
},
},
})
Comment please if someone check it. I don't know are it works.
I was wondering if there is a way of creating computed props programatically, while still accessing the instance to achieve dynamic values
Something like that (this being undefined below)
<script>
export default {
computed: {
...createDynamicPropsWithTheContext(this), // helper function that returns an object
}
}
</script>
On this question, there is a solution given by Linus: https://forum.vuejs.org/t/generating-computed-properties-on-the-fly/14833/4 looking like
computed: {
...mapPropsModels(['cool', 'but', 'static'])
}
This works fine but the main issue is that it's fully static. Is there a way to access the Vue instance to reach upon props for example?
More context
For testing purposes, my helper function is as simple as
export const createDynamicPropsWithTheContext = (listToConvert) => {
return listToConvert?.reduce((acc, curr) => {
acc[curr] = curr
return acc
}, {})
}
What I actually wish to pass down to this helper function (via this) are props that are matching a specific prefix aka starting with any of those is|can|has|show (I'm using a regex), that I do have access via this.$options.props in a classic parent/child state transfer.
The final idea of my question is mainly to avoid manually writing all the props manually like ...createDynamicPropsWithTheContext(['canSubmit', 'showModal', 'isClosed']) but have them populated programatically (this pattern will be required in a lot of components).
The props are passed like this
<my-component can-submit="false" show-modal="true" />
PS: it's can-submit and not :can-submit on purpose (while still being hacked into a falsy result right now!).
It's for the ease of use for the end user that will not need to remember to prefix with :, yeah I know...a lot of difficulty just for a semi-colon that could follow Vue's conventions.
You could use the setup() hook, which receives props as its first argument. Pass the props argument to createDynamicPropsWithTheContext, and spread the result in setup()'s return (like you had done previously in the computed option):
import { createDynamicPropsWithTheContext } from './props-utils'
export default {
⋮
setup(props) {
return {
...createDynamicPropsWithTheContext(props),
}
}
}
demo
If the whole thing is for avoiding using a :, then you might want to consider using a simple object (or array of objects) as data source. You could just iterate over a list and bind the data to the components generated. In this scenario the only : used are in the objects
const comps = [{
"can-submit": false,
"show-modal": true,
"something-else": false,
},
{
"can-submit": true,
"show-modal": true,
"something-else": false,
},
{
"can-submit": false,
"show-modal": true,
"something-else": true,
},
]
const CustomComponent = {
setup(props, { attrs }) {
return {
attrs
}
},
template: `
<div
v-bind="attrs"
>{{ attrs }}</div>
`
}
const vm = Vue.createApp({
setup() {
return {
comps
}
},
template: `
<custom-component
v-for="(item, i) in comps"
v-bind="item"
></custom-component>
`
})
vm.component('CustomComponent', CustomComponent)
vm.mount('#app')
<script src="https://unpkg.com/vue#3"></script>
<div id="app">{{ message }}</div>
Thanks to Vue's Discord Cathrine and skirtle folks, I achieved to get it working!
Here is the thread and here is the SFC example that helped me, especially this code
created () {
const magicIsShown = computed(() => this.isShown === true || this.isShown === 'true')
Object.defineProperty(this, 'magicIsShown', {
get () {
return magicIsShown.value
}
})
}
Using Object.defineProperty(this... is helping keeping the whole state reactive and the computed(() => can reference some other prop (which I am looking at in my case).
Using a JS object could be doable but I have to have it done from the template (it's a lower barrier to entry).
Still, here is the solution I came up with as a global mixin imported in every component.
// helper functions
const proceedIfStringlean = (propName) => /^(is|can|has|show)+.*/.test(propName)
const stringleanCase = (string) => 'stringlean' + string[0].toUpperCase() + string.slice(1)
const computeStringlean = (value) => {
if (typeof value == 'string') {
return value == 'true'
}
return value
}
// the actual mixin
const generateStringleans = {
created() {
for (const [key, _value] of Object.entries(this.$props)) {
if (proceedIfStringlean(key)) {
const stringleanComputed = computed(() => this[key])
Object.defineProperty(this, stringleanCase(key), {
get() {
return computeStringlean(stringleanComputed.value)
},
// do not write any `set()` here because this is just an overlay
})
}
}
},
}
This will scan every .vue component, get the passed props and if those are prefixed with either is|can|has|show, will create a duplicated counter-part with a prefix of stringlean + pass the initial prop into a method (computeStringlean in my case).
Works great, there is no devtools support as expected since we're wiring it directly in vanilla JS.
I am building a web app with nuxt.
here's simplified code:
pages/index.vue
data() {
return {
item: {name:'', department: '', testField: '',},
}
}
async asyncData() {
const result = call some API
const dataToInitialize = {
name: result.username,
department: result.department,
testField: //want to assign computed value
}
return {item: dataToInitialize}
}
Inside asyncData, I call API and assign value to dataToInitialize.
dataToInitialize has testField field, and I want to assign some computed value based on username and department.
(for example, 'a' if name starts with 'a' and department is 'management'..etc there's more complicated logic in real scenario)
I have tried to use computed property , but I realized that asyncData cannnot access computed.
Does anyone know how to solve this?
Any help would be appreciated!
=======
not sure if it's right way, but I solved the issue by setting 'testfield' inside created.
created() {
this.item.testField = this.someMethod(this.item);
},
Looking at the Nuxt lifecyle, you can see that asyncData is called before even a Vue instance is mounted on your page.
Meanwhile, fetch() hook is called after. This is non-blocking but more flexible in a lot of ways.
An alternative using fetch() would look like this
<script>
export default {
data() {
return {
staticVariable: 'google',
}
},
async fetch() {
await this.$axios(this.computedVariable)
},
computed: {
computedVariable() {
return `www.${this.staticVariable}.com`
},
},
}
</script>
Another alternative, would be to use URL query string or params, thanks to Vue-router and use those to build your API call (in an asyncData hook).
Here is an example on how to achieve this: https://stackoverflow.com/a/68112290/8816585
EDIT after comment question
You can totally use a computed inside of a fetch() hook indeed. Here is an example on how to achieve this
<script>
export default {
data() {
return {
test: 'test',
}
},
async fetch() {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${this.nice}`)
console.log(await response.json())
},
computed: {
nice() {
return this.test + 'wow!'
},
},
}
</script>
I found that destructuring fetch({}) causes issues with accessing this inside fetch scope ->
async fetch({ store, $anyOtherGlobalVar }){
store.dispatch...
// destructuring approach changes the scope of the function and `this` does not have access to data, computed and e.t.c
}
If you want to access this scope for example this.data, avoid destructuring and access everything through this.
async fetch() {
this.$store...
this.data...
}
I use vuex for my state as well as fetching data and display it in my application.
But I wonder if I'm doing it right. At the moment I dispatch an fetchDataAsync action from the component mounted hook, and I have an getter to display my data. Below is a code example of how I do it currently.
I wonder if it's necessary. What I really want is a getter, that looks at the state, checks if the data is already there and if the data is not there it is able to dispatch an action to fetch the missing data.
The API of vuex does not allow it so I need to put more logic into my components. E.g. if the data is depended of a prop I need a watcher that looks at the prop and dispatches the fetchDataAsync action.
For me it just feels wrong and I wonder if there is a better way.
let store = new Vuex.Store({
state: {
posts: {}
},
mutations: {
addPost(state, post) {
Vue.set(state.posts, post.id, post);
}
},
actions: {
fetchPostAsync({ commit }, parameter) {
setTimeout(
() =>
commit("addPost", { id: parameter, message: "got loaded asynchronous" }),
1000
);
}
},
getters: {
// is it somehow possible to detect: ob boy, I don't have this id,
// I'd better dispatch an action trying to fetch it...?
getPostById: (state) => (id) => state.posts[id]
}
});
new Vue({
el: "#app",
store,
template : "<div>{{ postToDisplay ? postToDisplay.message : 'loading...' }} </div>",
data() {
return {
parameter: "a"
};
},
computed: {
...Vuex.mapGetters(["getPostById"]),
postToDisplay() {
return this.getPostById(this.parameter);
}
},
methods: {
...Vuex.mapActions(["fetchPostAsync"])
},
mounted() {
this.fetchPostAsync(this.parameter);
}
});
I also created a codepen
Personally I think the solution you suggested (adding a watcher that dispatches fetchPostAsync if the post is not found) is the best one. As another commenter stated, getters should not have side effects.
I'm saving an array into local storage
and adding/removing from the array like.
I want the count of the array to update in the component as and when new items get added to the array in localstorage
I am using a computed property:
numOfCodes: {
// getter
get: function() {
let storageItems = localStorage.getItem("items");
if (storageItems) {
var items = JSON.parse(storageItems);
return items.length;
}
return 0;
}
}
The count is not changing as expected. it remains the same.
I have tried using vuex, but still have the issue. the goal is having the value react to the localstorage change
I think a solution to this would be to use vuex, I've mocked up an example below:
On your component:
computed: {
...mapGetters({
itemsCount: 'mockLocalStorage/itemsCount'
})
},
created() {
this.setItems(...);
},
methods: {
...mapActions({
setItems: 'mockLocalStorage/setItems'
})
}
In vuex:
state = {
items: []
};
getters = {
itemsCount: state => state.items.length
};
actions: {
setItems({ commit }, items) {
localStorage.setItem('items', items);
commit('setItems', items);
}
};
this.itemsCount would then be reactive in your component, and you could create a few more actions to add and remove individual items.
The localStorage does not share the reactivity system of Vue. This whole process is handled by Vue itself. See also here. I think you should be able to manually trigger a re-render by forcing Vue to update all of its components using forceUpdate. However, keep in mind that you would have to trigger the re-render whenever you update the localStorage or whenever you expect it to be updated.
Use a watcher.
props: ['storageItems', 'itemsLength'],
watch: {
storageItems: function(newVal, oldVal) {
this.storageItems = newVal
this.itemsLength = newVal.length
}
}