Laravel & Inertia.js - reactivity problem (vue3) - javascript

could it be that Inertia.js page components are blocking the reactivity of vue?
I have a Page component, in this component is a normal single file component.
I have a function that adds items to the ItemsManager.items object.
When I'm running this function the single component below doesnt adds this items in the v-for.
But when I'm reload the Page Component it works and the previously added items appear.
Here the single file component:
<template>
<div>
<div v-for="item in items" :key="item.$key">
test
</div>
</div>
</template>
<script>
import { ItemsManager } from "./utils.js";
export default {
name: "test-component",
data: () => ({
items: ItemsManager.items
}),
};
</script>
utils.js:
export const ItemsManager = {
items: [],
add(item) {
item.$key = this.items.length;
this.items.unshift(item);
},
};
function that adds the items (in page component):
addItem(title, options) {
ItemsManager.add({
name: title,
options: options
});
},
Thanks in advance!

Since you're using Vue2, you need to know that there are some caveats when adding/deleting things to Objects/Arrays. You don't show any code relevant to your actual way of adding stuff to your object, but I can still recommend that you'd check this page to understand and fix your issue.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

Related

Trigger a function in already mounted Vue component from router-link in parent component

I have a tricky communication issue between a router-link in my parent Vue component and a child component.
I would like to clear an input field on the child component, which is the main Index for my app. This component loads by default.
The parent App component contains the main navigation which contains a router-link routed back to the index like so:
<router-link :to="{ name: 'Index', query: { search: '' }}" class="nav-link">
Index
</router-link>
The Index component contains a search input to filter the index items list. The search value is tracked by the component's data.
data() {
return {
items: [],
tags: [],
search:'',
}
The issue is I cannot seem to clear the search input when the router-link for the Index is clicked. I assume because the Index is already mounted clicking the link cannot trigger any function in the component.
I have tried using $emit by wrapping the link text in <span #click="clearSearch()">, putting the $emit in the clearSearch function and picking it up in the Index, but this apparently doesn't work from parent to child??
As you can see in the router-link above I have also tried passing as query like query: { search: '' } and adding the following to the main.js route configuration.
props(route) {
return { search: route.query.search }
}
This sends the empty string in URL but I still cannot pick it up in the Index component to clear the search input, again I think because it is already mounted.
This seems like it should be an easy thing to do, but I am a bit stumped on it and don't know what else to try. I am still learning Vue so there might be something obvious I am missing. If anyone can help me out on how to achieve this I would much appreciate it. Thanks in advance.
An easy hack would be to use router.push() in a method instead of <router-link> and to make search a prop and clear it from the parent.
This way you can store search in the parent data and clear it on click.
// Parent component
<template>
<div #click="navigate" class="nav-link">
Index
</div>
...
<Index :search="search" />
</template>
<script>
export default {
data() {
return {
search: ''
}
},
methods: {
navigate() {
this.$router.push({ name: 'Index' });
this.search = '';
}
}
}
</script>
// Child component
<template>
{{ search }}
</template>
<script>
export default {
props: {
search: {
type: String,
default: '',
}
}
}
</script>

Vue 2 not setting component data when page is refreshed

I am having a rather strange problem with vue (v2.6.14) in which I am creating a new array based on one receive as a prop. Here's the relevant code:
props: { employees: Array },
data() {
return {
sortedEmployees: [],
};
},
mounted() {
this.sortedEmployees = this.employees.slice(0);
},
Essencially what I want here is to create a new reference for the employees array so that I can sort it to display later without actually altering the original array. I am not worried about cloning the actual objects inside the array since I will not alter them.
The problem with this is that, when the app 'hot reloads'(due to some change in the code) it works as expected, the hook is called and the component data is set according to what is expected BUT if I actually refresh the page in the browser, even though the hook is called, the component data is not set and I end up with an empty array. I can solve this by setting up a watcher to the prop and then it would set the data there, but what I am interested here is understanding what's happening. If the hook is called when the page is refreshed why doesn't it set the data properly as it does when 'hot reloading'?
I have a minimal setup created with vue-cli, no fancy configurations whatsoever. Any clue what I might be missing?
I guess the employees are loaded async, right?
I don't know your exact application structure, but the problem is usually the following:
The mounted hook gets called, when the component mounts, of course. If the employees are loaded async in the parent component, the mount hook is called, before the async call is resolved. So it will copy an empty array at this time.
With a watcher you solve this problem, because the watcher fires as soon as the async call resolves (as it mutates the employees array).
Same happens to the hotreload. When the hotreload occurs, the mounted hook gets executed again - at this time the employees array is already prefilled with values and therefore the correct array is copied in the mount hook.
Update
If you want to avoid watchers, you could also wrap your component like this:
<your-component
v-if="employees.length > 0"
:employees="employees"
/>
Be aware, that the copied employees array IN your component is still not reactive. It just copies the array on the first time it has more than 1 value. A watcher really makes sense in this case.
If you use watchers, make sure to use the immediate: true option. This ensures, that the watcher is also called the first time on render (and also on hotreload).
Summary
If you really want to copy the array, use watchers (with the immediate: true flag).
If it's just about sorting, go for the computed property solution suggested by #Sebastian Scholl
It sounds like the component is Refreshing (reseting) with the prop change however it's not Re-mounting. This is what would cause the array to reset to it's default state ([]), whereas on hot-reload the actual page is reloading.
Try the following, and if it doesn't resolver the issue I would suggest going the route of using a Watcher.
<template>
<div>
sorted: {{ sortedEmployees }}
<br />
not sorted: {{ employees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data() {
return {
sortedEmployees: Array.from(this.employees),
};
}
};
</script>
Another approach is to just use a Computed method so and add any filtering/sorting logic inside that method. It would be something like:
<template>
<div>
<input v-model="sortKey" />
sorted: {{ sortedEmployees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data () {
return {
sortKey: ''
}
},
computed: {
sortedEmployees() {
return Array.from(this.employees).sort(this.sortingFunction);
},
},
methods: {
sortingFunction(a, b) {
// sorting function using this.sortKey
}
}
};
</script>
UPDATED ANSWER
I switched up the code in the example a little and believe to have gotten it to work as I you're describing.
App.js
First off, I made it so that the employees array is updated after 3 seconds.
<template>
<div id="app">
<Dashboard :employees="employees" />
</div>
</template>
<script>
import Dashboard from './components/Dashboard.vue';
export default {
name: 'App',
components: { Dashboard },
data () {
return {
employees: [
{
employeeId: '1',
firstName: 'Leite',
}
]
};
},
methods: {
updateEmployees () {
this.employees = this.employees.concat([
{
employeeId: '2',
firstName: 'Jacinto',
},
{
employeeId: '3',
firstName: 'Capelo',
}
]);
}
},
mounted () {
setTimeout(this.updateEmployees, 3000)
},
};
</script>
Dashboard.js
The updated() lifecycle hook runs whenever data changes are detected (props and data properties). This effectively detected the change in the prop passed by the parent App.js component and re-rendered the data - whereas the mounted hook only ran once per page load.
<template>
<div id="dashbord">
<div
v-for="(employee, index) in sortedEmployees"
:key="employee.employeeId"
>
{{ employee.firstName }}
</div>
</div>
</template>
<script>
export default {
name: 'Dashboard',
props: {
employees: Array
},
data() {
return {
sortedEmployees: Array.from(this.employees)
};
},
updated() {
this.sortedEmployees = Array.from(this.employees)
}
};
</script>

TypeError: Cannot read property '_wrapper' of undefined

I am trying to update the props data sent to a component on a button click to a single component in vue.
Button click triggers an action loads the data from a config. But this throws the error and the error message was not clear. Find the error here https://imgur.com/a/0psUWKr
If I pass the data directly without the button actions, it works fine.
My Main component
<template>
<div>
<MyList v-if="listItems" :elements="listItems"/>
<button #click="showSlider">Show Slider</button>
</div>
</template>
<script>
// imports the components and config files
export default {
name: "ListView",
data() {
return {
listItems: []
};
},
components: {
MyList
},
methods: {
showSlider: function() {
this.listItems.push(configs['elements'])
},
</script>
NOTE: If i provide the data to listItems by default it works
And MyList file
<template>
<ul>
<li v-for="each in elements" :key="each.id">
{{each.name}}
</li>
</ul>
<template>
<script>
// imports the components and config files
export default {
name: "MyList",
props: {
elements: {
type: Array
}
}
</script>
It should work, in general. But in my case, the issue is with push in the following function.
showSlider: function() {
this.listItems.push(configs['elements'])
},
Storing the same from Vuex and using dispatcher to update the same works like a charm. And I use computed property to load the state from the vueX and pass it to MyList

Vuex how to Access state data on component mounted or created hook?

I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)
I copied your code and tested it ( of-course I created my own dummy notes so I could remove the get request ) and I was able to get the notes display on a page.
A couple of things that I realized from your code, you may need to add a store property as there are places in your component ( test ) where you are referencing it, yet you only define it on the 'app' component. So in this section of your code modify as shown below:
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
store : store
}
},
The key difference is the definition of the 'store' property. Please note that, what you have done, defining a "store" property in your app component, is correct, but the very same needs to be defined in "test" component as I have shown in the above code snippet above.
Second thing is, you are using $store and I guess that gives you undefined, unless as you said, in the libraries that you included this resolves accordingly, but on my side I had to remove all references of "$store" and replace it with just "store" (without the dollar sign).
Lastly for testing purposes, I would advise you to also

vue.js list ( template ) binding not updating when changing data from directive

First of all : I'm using laravel spark and the given setup of vue that comes with spark.
I have a "home" component with the prop "custom". Within custom there's a "passwords" array. (Entry added by code of directive, it's initialized empty)
My component ( alist) which should be bound against the data
<template id="passwords-list-template">
<div class="password" v-for="password in list">
<ul>
<li>{{ password.name }}</li>
<li>{{ password.description }}</li>
</ul>
</div>
</template>
<script>
export default {
template: '#passwords-list-template',
props: ['list'],
};
</script>
Usage
<passwords-list :list="custom.passwords"></passwords-list>
Using vue devtools I can see that my data is updating, however my list is not. Also other bindings like
<div v-show="custom.passwords.length > 0">
Are not working ...
UPDATE : Parent component (Home)
Vue.component('home', {
props: ['user', 'custom'],
ready : function() {
}
});
Usage
<home :user="user" :custom="spark.custom" inline-template>
Update 2: I played around a little bit using jsfiddle. It seems like changing the bound data object using $root works fine for me when using a method of a component. However it does not work when trying to access it using a directive
https://jsfiddle.net/wa21yho2/1/
There were a lot of errors in your Vue code. First of all, your components where isolated, there wasn't an explicit parent-child relationship.Second, there were errors in the scope of components, you were trying to set data of the parent in the child, also, you were trying to set the value of a prop, and props are by default readonly, you should have written a setter function or change them to data. And finally, I can't understand why were you trying to use a directive if there were methods and events involve?
Anyway, I rewrote your jsfiddle, I hope that you find what you need there. The chain is Root > Home > PasswordList. And the data is in the root but modified in home, the last component only show it. the key here are twoWay properties, otherwise you wouldn't be able to modify data through properties.
Here is a snippet of code
Home
var Home = Vue.component('home', {
props: {
user: {
default: ''
},
custom: {
twoWay: true
}
},
components: {
passwordList: PasswordList
},
methods: {
reset: function () {
this.custom.passwords = [];
}
}
});
// template
<home :custom.sync="spark.custom" inline-template>
{{custom | json}}
<button #click="reset">
reset in home
</button>
<password-list :list="custom.passwords"></password-list>
<password-list :list="custom.passwords"></password-list>
</home>
Here is the full jsfiddle

Categories