Passing data to Vue child component without re-mounting it - javascript

I have a page that includes some audio visualization that's being done within a component. The component is only for displaying stuff, the audio logic is done in the parent.
I'll keep this part short, since my problem is more general, but basically I'm passing the AnalyserNode to the child to poll the realtime frequency analysis from within my Three.js render-loop in the child (WorkletModule).
So in my parent I have something like this:
<template>
...
<WorkletModule
:analyser="analyser"
:key="componentKey"
/>
...
</template>
...
data() {
return {
componentKey: 0,
analyser: null,
};
methods: {
startRecording() {
this.componentKey++
..
this.analyser = audioContext.createAnalyser();
..
}
}
while my WorkletModule.vue looks like this:
props: {
analyser: {
type: AnalyserNode
},
},
mounted() {
//initialize graphics, etc..
}
//in the render loop I need to do something like this:
this.analyser.getByteFrequencyData(soundData);
Now if I use it like above, it does actually work, however, every call to startRecording() causes the WorkletModule to re-mount, creating a hick-up and emptying the canvas of the WorkletModule.
If I remove the ":key="componentKey" it doesn't get updated, which means that the reference to the analyzer in the WorkletModule/prop always stays null, which is obviously not what I want.
Is there a way to pass the analyzer object in another form than a prop?
Basically I need the analyzer value in every frame of the render-loop. I could not pass it as a prop but rather emit an event from the render loop of the WorkletModule that in turn gets the parent to send the current values back to the child via another event. That doesn't seem like a very elegant thing though, so I guess there's a better way.

I'm really puzzled by this:
If I remove the ":key="componentKey" it doesn't get updated, which means that the reference to the analyser in the WorkletModule/prop always stays null
This is not supposed to happen since props are updated without re-rendering by default. Of course, if you use a key, the component is fully destroyed and recreated on every key update.
You could listen to your props update by using a watcher.
In the example below, I pass a prop to a children component, from null to a number. The component is only rendered once and the prop is updated as expected. I only added a watcher to track every update of the said prop and give me the possibility to add any logic from that. You could try to do that in order to understand if the prop is really never updated or maybe your implementation lacks something (and you didn't show us the malicious bit of code that drives you into thinking it's not updated).
const test = Vue.component("test", {
props: ["prop1"],
methods: {
getProp() {
console.log(this.prop1);
}
},
created() {
console.log("created"); // only called once
setInterval(this.getProp, 2000);
},
watch: {
prop1(newVal, oldVal) {
console.log("watcher:", newVal, oldVal);
// do something
}
},
template: "<div>test</div>"
});
new Vue({
el: "#app",
components: {
test
},
data: {
toProp: null
},
methods: {
setProp() {
this.toProp = new Date().valueOf();
}
},
created() {
setInterval(this.setProp, 6000);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test :prop1="toProp" />
</div>

Related

Vue Reactivity: Why replacing an object's property (array) does not trigger update

I have a Vue app where I'm trying to make a thin wrapper over the Mapbox API. I have a component which has some simple geojson data, and when that data is updated I want to call a render function on the map to update the map with that new data. A Vue watcher should be able to accomplish this. However, my watcher isn't called when the data changes and I suspect that this is one of the cases that vue reactivity can't catch. I'm aware that I can easily fix this problem using this.$set, but I'm curious as to why this isn't a reactive update, even though according to my understanding of the rules it should be. Here's the relevant data model:
data() {
return{
activeDestinationData: {
type: "FeatureCollection",
features: []
}
}
}
Then I have a watcher:
watch: {
activeDestinationData(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
}
Finally, down in my app logic, I update the features on the activeDestination by completely reassigning the array to a new array with one item:
// Feature is a previously declared single feature
this.activeDestinationData.features = [feature];
For some reason the watcher is never called. I read about some of the reactivity "gotchas" here but neither of the two cases apply here:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
What am I missing here that's causing the reactivity to not occur? And is my only option for intended behavior this.set() or is there a more elegant solution?
as default vue will do a shallow compare, and since you are mutating the array rather than replacing, its reference value is the same. you need to pass a new array reference when updating its content, or pass the option deep: true to look into nested values changes as:
watch: {
activeDestinationData: {
handler(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
deep: true
}
}
If you need to watch a deep structure, you must write some params
watch: {
activeDestinationData: {
deep: true,
handler() { /* code... */ }
}
You can read more there -> https://v2.vuejs.org/v2/api/#watch
I hope I helped you :)

Vue Router - call function after route has loaded

I'm working on a project where I need to call a function AFTER the route has finished loading. However, when using the 'watch' functionality, it only loads on route change, but does so before route has finished loading. So when I attempt to run a script that targets DOM elements on the page, those elements don't exist yet. Is there any functionality in Vue Router that would allow me to wait until everything is rendered before running the script?
const app = new Vue({
el: '#app',
router,
watch: {
'$route': function (from, to) {
function SOMEFUNCTION()
}
},
data: {
some data
},
template: `
<router-view/>
`
})
You should use Vue.nextTick
In your case this would translate to:
const app = new Vue({
el: '#app',
router,
watch: {
$route() {
this.$nextTick(this.routeLoaded);
}
},
data() {
return {};
},
methods: {
routeLoaded() {
//Dom for the current route is loaded
}
},
mounted() {
/* The route will not be ready in the mounted hook if it's component is async
so we use $router.onReady to make sure it is.
it will fire right away if the router was already loaded, so catches all the cases.
Just understand that the watcher will also trigger in the case of an async component on mount
because the $route will change and the function will be called twice in this case,
it can easily be worked around with a local variable if necessary
*/
this.$router.onReady(() => this.routeLoaded());
},
template: `<router-view/>`
})
This will call the routeLoaded method every time the route changes (which I'm deducing is what you need since you are using the <router-view> element), if you also want to call it initially, I would recommend the mounted hook (like in the example) or the immediate flag on the watcher
In my opinion on this situation, you should use component life cycle method of the loaded component, either use mounted method or created method.
or if your script doesn't depend on any vue component (store) you can use router.afterEach hook
router.afterEach((to, from) => { if (to.name !== 'ROUTENAME'){ // do something }});
The solution for me was to set up a custom event in every page's mounted() hook with a mixin and listen for that event on the body for example. If you wanted to strictly tie it with the router's afterEach or the route watcher to ensure the route has indeed changed before the event was fired, you could probably set up a Promise in the afterEach and resolve it in the page's mounted() by either the event or sharing the resolve function through the window.
An example:
// Component.vue
watch: {
'$route': function (from, to) {
new Promise((resolve) => {
window.resolveRouteChange = resolve;
}).then(() => {
// route changed and page DOM mounted!
});
}
}
// PageComponent.vue
mounted() {
if(window.resolveRouteChange) {
window.resolveRouteChange();
window.resolveRouteChange = null;
}
}
In case of router-view, we can manually detect router-view.$el change after $route is changed
watch: {
'$route'(to, from) {
// Get $el that is our starting point
let start_el = this.$refs.routerview.$el
this.$nextTick(async function() { await this.wait_component_change(start_el)})
}
},
methods: {
on_router_view_component_changed: function() { }
wait_component_change: async function(start_el) {
// Just need to wait when $el is changed in async manner
for (let i = 0; i < 9; i++) {
console.log('calc_has_dragscroll ' + i)
if(start_el) {
if (!start_el.isSameNode(this.$refs.routerview.$el)) {
// $el changed - out goal completed
this.on_router_view_component_changed()
return
}
}
else {
// No start_el, just wait any other
if(this.$refs.routerview.$el) {
// $el changed - out goal completed too
this.on_router_view_component_changed()
return
}
}
await this.$nextTick()
}
},
}
You can accomplish this by hooking into VueJS lifecycle hooks:
Use VueJS Lifecycle Hooks:
Here is a summary of the major VueJS lifecycle hooks. Please consult the documentation for the full description.
i. beforeCreate: This function will be called before the component is created
ii. created: This function will be called after the component is created, but note although the component is created, it hasn't been mounted yet. So you won't be able to access the this of the component. However, this is a good place to make Network Requests that will update the data properties.
iii. mounted: This function is called once the component has been rendered and the elements can be accessed here. This is what you're looking for.
iv. beforeDestroy: This function is called before the component is destroyed. This can be useful to stop any listeners (setTimeout, setInterval..), that you created.
See the diagram below for the details.
const app = new Vue({
el: '#app',
router,
mounted(){
this.someFunction()
},
data: {
some data
},
template: `
<router-view/>
`
})
Use Vue Router Navigation Guards: Vue Router also expose some lifecycle hooks that can you hook into. However, as you will see below they do not fit your requirements:
i. beforeRouteEnter: called before the route that renders this component is confirmed. oes NOT have access to this component instance, because it has not been created yet when this guard is called!
ii. beforeRouteUpdate: called when the route that renders this component has changed, but this component is reused in the new route.
iii. beforeRouteLeave: called when the route that renders this component is about to be navigated away from.
References:
VueJS Documentation (LifeCycle): VueJS Instance
Vue Router Documentation (Navigation Guards): Navigation Guards

How to make data from localStorage reactive in Vue js

I am using localStorage as a data source in a Vue js project. I can read and write but cannot find a way to use it reactively. I need to refresh to see any changes I've made.
I'm using the data as props for multiple components, and when I write to localStorage from the components I trigger a forceUpdate on the main App.vue file using the updateData method.
Force update is not working here. Any ideas to accomplish this without a page refresh?
...............
data: function () {
return {
dataHasLoaded: false,
myData: '',
}
},
mounted() {
const localData = JSON.parse(localStorage.getItem('myData'));
const dataLength = Object.keys(localData).length > 0;
this.dataHasLoaded = dataLength;
this.myData = localData;
},
methods: {
updateData(checkData) {
this.$forceUpdate();
console.log('forceUpdate on App.vue')
},
},
...............
Here's how I solved this. Local storage just isn't reactive, but it is great for persisting state across refreshes.
What is great at being reactive are regular old data values, which can be initialized with localStorage values. Use a combination of a data values and local storage.
Let's say I was trying to see updates to a token I was keeping in localStorage as they happened, it could look like this:
const thing = new Vue({
data(){
return {
tokenValue: localStorage.getItem('id_token') || '',
userValue: JSON.parse(localStorage.getItem('user')) || {},
};
},
computed: {
token: {
get: function() {
return this.tokenValue;
},
set: function(id_token) {
this.tokenValue = id_token;
localStorage.setItem('id_token', id_token)
}
},
user: {
get: function() {
return this.userValue;
},
set: function(user) {
this.userValue = user;
localStorage.setItem('user', JSON.stringify(user))
}
}
}
});
The problem initially is I was trying to use localStorage.getItem() from my computed getters, but Vue just doesn't know about what's going on in local storage, and it's silly to focus on making it reactive when there's other options. The trick is to initially get from local storage, and continually update your local storage values as changes happen, but maintain a reactive value that Vue knows about.
For anyone facing the same dilemma, I wasn't able to solve it the way that I wanted but I found a way around it.
I originally loaded the data in localStorage to a value in the Parent's Data called myData.
Then I used myData in props to populate the data in components via props.
When I wanted to add new or edit data,
I pulled up a fresh copy of the localStorage,
added to it and saved it again,
at the same time I emit the updated copy of localStorage to myData in the parent,
which in turn updated all the data in the child components via the props.
This works well, making all the data update in real time from the one data source.
As items in localstorage may be updated by something else than the currently visible vue template, I wanted that updating function to emit a change, which vue can react to.
My localstorage.set there does this after updating the database:
window.dispatchEvent(new CustomEvent('storage-changed', {
detail: {
action: 'set',
key: key,
content: content
}
}));
and in mounted() I have a listener which updates forceRedraw, which - wait for it - force a redraw.
window.addEventListener('storage-changed', (data) => {
this.forceRedraw++;
...

Wait until parent component is mounted / ready before rendering child in vue.js

In my SPA app, I have an <app-view> wrapper which handles base app code (load user data, render navbar and footer, etc) and has a slot for rendering the actual page. This slot is rendered only if the user data is available.
This wrapper was created because some pages needed a different base code, therefore I couldn't keep this base code in the main app containing <router-view> anymore.
I tried looking if vue-router provides advanced options or suggests a design pattern for switching base code, didn't find anything.
The problem is that the child component will be rendered before the parent component is mounted, i.e. before the parent decides not to render the child component (because it's loading user data). This causes errors like undefined as no attribute foo.
Because of that, I'm looking for a way to defer child rendering until its parent is mounted.
I had a similar problem though not with a SPA. I had child components that needed data from the parent. The problem is that the data would only be generated after the parent has finished mounting so I ended up with null values in the children.
This is how I solved it. I used v-if directive to mount the children only after the parent has finished mounting. (in the mounted() method) see the example below
<template>
<child-component v-if="isMounted"></child-component>
</template>
<script>
data() {
isMounted: false
}, mounted() {
this.isMounted = true
}
</script>
After that, the child could get the data from the parent.
It is slightly unrelated but I hope it gives you an idea.
After trying a few options, it looks like I need to bite the bullet and explicitly define the data that my components depend on, like so:
<app-view>
<div v-if='currentProfile'>
...
</div>
</div>
(currentProfile is received from vuex store getter, and is fetched within app-view)
For any of you that wants to show the child component as soon as the parent components gets data from an API call then you should use something like this:
<template>
<child-component v-if="itemsLoaded"></child-component>
</template>
<script>
data() {
itemsLoaded: false
},
methods: {
getData() {
this.$axios
.get('/path/to/endpoint')
.then((data) => {
// do whatever you need to do with received data
// change the bool value here
this.itemsLoaded = true
})
.catch((err) => {
console.log(err)
})
},
},
mounted() {
this.getData()
// DONT change the bool value here; papa no kiss
this.itemsLoaded = true
}
</script>
If you try to change the boolean value this.itemsLoaded = true in the mounted() method, after calling the getData() method, you will get inconsistent results, since you may or may not receive the data before the this.itemsLoaded = true is executed.
You can actually put the v-if on the <slot> tag in your component.
new Vue({
el: '#app',
render: function(createElement) {
return createElement(
// Your application spec here
{
template: `<slotty :show="showSlot"><span> here</span></slotty>`,
data() {
return {
showSlot: false
}
},
components: {
slotty: {
template: `<div>Hiding slot<slot v-if="show"></slot>.</div>`,
props: ['show']
}
},
mounted() {
setTimeout(() => this.showSlot = true, 1500);
}
}
);
}
})
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
</div>

Binding a UI element with a Vuex store

I'm trying to bind a UI element (a single-line textbox or 'input' element) with a Vuex store. This fiddle has the code.
When the SearchResult component is visible, it auto-updates -- see the GIF below, where Lisp or Prolog is typed. That's not what I'd like to happen. What I'd really like to do is decouple the UI state (i.e. the value of the textbox) from the model's state, so that if I type Lisp and press Search, the SearchResult component updates itself.
Ideally I'd like to bind the textbox with a variable that's not in the store, but also add some code to observe changes to the store, so that any changes to the store are reflected in the UI.
I read the forms handling documentation for Vuex but wasn't very clear about the best way to get this done. Please could anyone help? I'm new to SPAs so I'm sure there's a better way of getting this done.
I think the approach you have used is the general approach if you want to use a store variable in input. Given that you want to decouple the UI variable with the model's state(Why?), you can do following:
Have a local variable in that vue instace
use that local variable with v-model
put a watch on state variable, if state variable changes, change local variable.
set state variable on button press, or some other way like onblur event
Here are relevant JS changes:
const app = new Vue({
router,
el: '#app',
data: {
localQuery: ''
},
computed: {
query: {
get () { return store.state.query },
set (v) { store.commit('setquery', v) }
}
},
methods: {
s1: function () {
console.log('app.s1 this.query: ' + this.query);
this.query = this.localQuery
router.push({ name: 'qpath', params: { query: this.query }});
}
},
watch:{
query: function (newVal) {
this.localQuery = newVal
}
}
})
see updated fiddle here.

Categories

Resources