Vue: Accessing data from an unmounted component - javascript

I have an issue where I want to retreive data from a child component, but the parent needs to use that data, before the child is mounted.
My parent looks like this
<template>
<component :is="childComp" #mounted="setData"/>
</template>
<script>
data : {
childComp : null,
importantData : null
},
methods : {
addComponent : function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData : function(value) {
this.importantData = value;
},
prepareToAdd : function(importantData){
//something that has to be run before childComp can be mounted.
}
}
</script>
My child (or rather, all the potential children) would contain something like this:
<script>
data : {
importantData : 'ABC',
},
created: function() {
this.$emit('mounted', this.importantData);
},
</script>
This clearly doesn't work - importantData is set when the childComponent is mounted, but prepareToAdd needs that data first.
Is there another way of reaching in to the child component and accessing its data, before it is mounted?

You can use $options to store your important data and have it available in beforeCreate. You can also use it to initialize a data item, and you can emit data items in created (you don't have to initialize from $options to emit in created, I'm just pointing out two things that can be done). The $options value is, itself, reactive (to my surprise) and can be used like any data item, with the added benefit that it is available before other data items.
new Vue({
el: '#app',
methods: {
doStuff(val) {
console.log("Got", val);
}
},
components: {
theChild: {
template: '<div>Values are {{$options.importantData}} and {{otherData}}</div>',
importantData: 'abc',
data() {
return {
otherData: this.$options.importantData
};
},
beforeCreate() {
this.$emit('before-create', this.$options.importantData);
},
created() {
this.$emit('on-created', this.otherData + ' again');
// It's reactive?!?!?
this.$options.importantData = 'changed';
}
}
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<the-child #before-create="doStuff" #on-created="doStuff"></the-child>
</div>

My bad :(
We cannot get the data inside beforeCreated() hook.
Use the beforeCreate() hook instead of created() hook:
beforeCreate: function() {
this.$emit('mounted', this.importantData);
},
We can use a watcher or computed option, so now your parent component would look:
data: {
childComp: null,
importantData: null,
isDataSet: false
},
methods: {
addComponent: function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData: function(value) {
this.importantData = value;
this.isDataSet = true;
},
prepareToAdd: function(importantData) {
//something that has to be run before childComp can be mounted.
}
},
watch: {
isDataSet: function(newValue, oldValue) {
if (newValue) {
this.addComponent();
}
}
}
I would suggest to use computed method as it caches the results. Use watcher when you have to perform asynchronous code.

Related

Best practice for implementing custom form in Vue

My goal is to create a custom form component called app-form which provides v-model to access the validation. For the input I want to detect is also a custom component called app-input.
Here is my implementation so far.
app-form
<template>
<div>
<slot></slot>
</div>
</template>
const acceptedTags = ['app-input'];
export default {
/*
props: value,
data: isValid
*/
updated() {
// update isValid whenever the view changes
this.checkValidation();
},
methods: {
checkValidation() {
this.isValid = true;
this.checkValidation(this);
this.$emit('input', this.isValid);
},
checkValidationRecursion(node) {
if (acceptedTags.includes(node.$options.name)) {
let result = node.checkValidation();
this.isValid &&= result;
}
node.$children.forEach((child) => this.checkValidationRecursion(child));
},
}
}
app-input
<input
:value="selfValue"
#input="onInput"/>
export default {
name: 'app-input',
/*
props: value,
data: selfValue,
*/
methods: {
checkValidation() {
let valid = true;
/*
this.rules = [(v) => !!v || 'Required'];
*/
for (let f of this.rules) {
let result = f(this.selfValue);
if (typeof result === 'string') {
valid = false;
}
}
return valid;
},
// onInput() => update selfValue and emit
},
// watch: update selfValue
}
In the code above, the app-form have to traverse the whole component tree to get the target inputs every time anything is updated. Is there any better way to achieve it?
For these Kind of things I like to use provide/inject https://v2.vuejs.org/v2/api/#provide-inject. The idea is to "provide" an object in a Parent-Component (your Form-Component) and to "inject" this object in any of your descandant Components. Data here is not reactive, but you can pass a reference to a reactive Object (for example a Vue Object).
If providing a Vue-Instance you can emit an event, like "check-validation" on that instance and your descandant components can listen to that and then emitting an validate-Event with the validation-Result back to the parent Component.
Here is a very basic Example: https://codepen.io/arossbach/pen/xxdxOVZ
Vue.component('my-form', {
provide () {
return {
formBus: this.formBus,
};
},
mounted() {
setTimeout(() => {
this.formBus.$emit("check-validation");
},4000);
this.formBus.$on("validate-element", isValid => {
this.isValidForm &&= isValid;
});
},
data () {
return {
formBus: new Vue(),
isValidForm: true,
};
},
template: '<div><slot></slot></div>',
});
Vue.component('child', {
inject: ['formBus'],
template: '<div></div>',
data() {
return {
isValid: true,
}
},
methods: {
validate() {
this.isValid = Boolean(Math.round(Math.random()));
this.formBus.$emit("validate-element", this.isValid);
}
},
created() {
this.formBus.$on("check-validation", this.validate);
}
});
new Vue({
el: '#app',
data () {
return {
};
},
});
HTML:
<div id="app">
<my-form>
<child></child>
<child></child>
</my-form>
</div>

How to pass object handler into vue component

I want to cache state inside a handler passed by props.
Problem is, the template renders and text content changes well, but the console always throw error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'apply' of undefined"
Codes below:
<template>
<picker-box :options="item.options" #change="handlePickerChange($event, item)">
<text>{{ item.options[getPickerValue(item)].text }}</text>
</picker-box>
</template>
<script>
export default {
props: {
item: {
type: Object,
default() {
return {
options: [{ text: 'op1' }, { text: 'op2' }],
handler: {
current: 0,
get() {
return this.current
},
set(value) {
this.current = value
},
},
/* I also tried this: */
// new (function () {
// this.current = 0
// this.get = () => {
// return this.current
// }
// this.set = (value) => {
// this.current = value
// }
// })(),
}
},
},
},
methods: {
handlePickerChange(value, item) {
item.handler.set(value)
},
getPickerValue(item) {
return item.handler.get()
},
},
}
</script>
I know it's easy using data() or model prop, but I hope to cahce as this handler.current, in other words, I just want to know why this handler object isn't correct (syntax layer), how can I fix it.
What exactly state are you trying pass?
If you need to keep track of a static property, you could use client's localstorage, or a watcher for this.
If you need to keep of more dynamic data, you could pass it to a computed property that keeps track and sets them everytime they change.

How to use data from one hook to other hook in Vue.js?

In my vue.js application I send request by axios package in created() hook. I add response to array called coordinates. I want to use that array outside of created() hook. For example in mounted() hook or in functions which we can set in methods.
Right now when I tried to use self.coordinates outside created() hook it return undefined. When I use this.coordinates it return just [__ob__: Observer].
Whats wrong I did?
export default {
name: "Map",
data() {
return {
coordinates: [],
}
},
created() {
let self = this;
axios.get('URL').then(function (response) {
let coordinates = [];
for (let i = 0; i < response.data.length; i++) {
coordinates.push([response.data[i]["LATITUDE"], response.data[i]["LONGITUDE"]]);
}
self.coordinates = coordinates;
});
},
mounted() {
console.log(self.coordinates); // undefined
consol.log(this.coordinates); // [__ob__: Observer]
},
}
I would prefer "mounted" and move the logic into methods for reusability. The method can be kicked from anywhere afterwards. In the example below, I prefered kicking the method direcly. Watchers is another option.
Here is the fiddle https://jsfiddle.net/dj79ux5t/2/
new Vue({
el: '#app',
data() {
return {
coordinates: []
}
},
mounted() {
let self = this;
axios.get('https://api.weather.gov/').then(function (response) {
self.coordinates = response.data;
self.greet();
});
},
methods: {
greet: function () {
console.warn(this.coordinates.status);
}
}
})
I think instead of mounted , you should use watch . You call some link so it will take time to load that data , watch method will trigger when your data is updated ...
watch: {
coordinates: {
handler: function (updateVal, oldVal) {
console.log(updateVal)
},
deep: true
}
},

Vue plugin, adding global component which takes data

I am using Vue plugins so that user can access a global component once registering the global component and configuring it inside Vue.use. For this I need to pass some data from Vue.use() to Component.vue.
Take a look at the following code:
Vue.use(MyPlugin, { data: true });
the code of MyPlugin is
import Plugin from './plugin';
const IconPlugin = {
install(Vue, options) {
console.log(options); // Here I can see {data: true}
Vue.component('GlobalComponent', Icon);
},
};
Now I need to pass this options variable to the component. So that a user whenever use
<GlobalComponent />
{data: true} should always be there.
Basically, that is a configuration which user is passing and the further component computation will be dependent on this.
You can use Vue.extend to extend components
var Icon = Vue.extend({
data: function() {
return {
foo: 'fooooo',
bar: 'barr'
}
},
template: '<div><button #click="doFooStuff">{{foo}}</button><button #click="doBarStuff">{{bar}}</button></div>',
methods: {
doBarStuff: function() {
console.log(this.foo, this.bar)
},
doFooStuff: function() {
console.log(this.foo)
}
}
})
const IconPlugin = {
install(Vue, options) {
// console.log(options);
// normalize data (select only props you want)
var data = Object.assign({}, options);
var NewIcon = Icon.extend({
data: function() {
return data;
}
})
Vue.component('GlobalComponent', NewIcon);
},
};
Vue.use(IconPlugin, {
foo: 'FOOO'
});
new Vue({
el: '#app',
components: {
Icon
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<icon></icon>
<global-component></global-component>
</div>
it sounds like you want to take a look at the component guide. It would seem that you want to merge the data with where you are passing Icon.

operating on props in vue.js components

Im pretty newm to vue and i'm trying to migrate the frontend of my laravel project to vue but i'm having an issue with it, i'm trying to loop through an array of provided objects called rooms and create divs for each of the in my component as well as setting the default room_id as the first id of the room. The problem is when is access the provided prop array called 'room' in the dom (html) it works flawlessly, but in my vue code for the component file it always seems to be undefined or empty
Here is my components vue code:
export default {
created() {
//this.loadMessages(this.room_id)
console.log(this.first_room) //undefined;
console.log(this.rooms) //empty array;
},
props: ['rooms','first_room'],
computes:{
myrooms: function(){
return this.first_room;
}
},
data()
{
return{
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
methods: {
loadMessages(id)
{
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
the important part of the component html
<div v-for="room in rooms">
<div class="chat-user room">
<div v-for="other in room.others">
<img class="chat-avatar img-circle" :src="other.image" alt="image" >
<div class="chat-user-name">
<a :href="'/user/' + other.id">{{ other.name}}</a>
</div>
</div>
</div>
</div>
//this all works, just for reference
method where i set the values passed to the prop in my main vue instance
EDIT: THE PARENT INSTANCE CODE
Oh and i cant seem too access the rooms array being passed as it is always empty IN code but it loops in the html
window.App.app= new Vue({
el: '#wrapper',
data: {
messages: [],
rooms: [],
test: 'yes',
first: ''
},
created() {
this.fetchRooms();
this.first = 1;
},
methods: {
fetchMessages(id) {
console.log(id);
},
fetchRooms()
{
axios.get('/rooms').then(response => {
this.rooms = response.data;
});
}
}
});
finally where i call my component
<chat-messages :rooms="rooms" :first_room="1"></chat-messages>
//variables referenced from main vue instance
I have literally torn most of my hair on this, please any help is appreciated
In the child component to which the props are passed on.
export default {
created() {
console.log(this.first_room) //undefined;
},
props: ['rooms','first_room'],
computed :{
myrooms: function(){
return this.first_room;
}
},
data () {
return {
messages: [],
newMessage: '',
room_id: 1 //for test purposes, this works,
}
},
watch: {
rooms (n, o) {
console.log(n, o) // n is the new value, o is the old value.
}
},
methods: {
loadMessages (id) {
axios.get('/messages/'+id).then(response => {
this.messages = response.data;
console.log(response.data);
});
}
}
}
You can add a watch on data properties or computed to see the change in their values.
In the question, (as what it appears to be the case), you have consoled the value of the props in the created lifecycle, the props' value gets changed by an API call in the parent component, after the creation of the child component. That explains why your template shows the data but not in the console in the created lifecycle hook.

Categories

Resources