Access higher level component (parent) properties from subsubcomponents (children of children) - vue.js

I am working on something which requires access to a property injected at the top level of a VueJS application. I can access the property by using:
this.$parent.$attrs.propertyname
This works nicely however, I am now wanting to be able to access the same item from a further subcomponent which works the same as above with an additional $parent call. Is there a way I can call this item at the top level in another way?
I have looked at $root and tested using Vue $vm console calls in the browser but doesnt work so far. I suspect theres a really easy way of doing this but I can't find it at the moment.
I have tried:
adding a return method from top level and calling it
$root
setting a prototype outside the vue application at the top level: Vue.prototype.$varname = '' and then attempting to assign on created() to this.$varname = this.mypropertyname
I suspect I am close somewhere or there is something I have missed.

I did plenty of fiddling and I have my answer.
So basically, my application passes data into the app from Laravel at the tag level. I have an overall template file for the app which is where I now set the data with a setter to root.
I will explain with some code:
Laravel:
<v-app token="abcdefg">
<template></template>
</v-app>
Thats the declaration of data passed into the app
On template.vue:
<script>
export default {
name: "template",
created() {
this.$root.setToken(this.$parent.$attrs.token);
}
}
</script>
We access the data and use the setter placed on app.js (root) to assign the data to an accessible $root location
app.js code:
const app = new Vue({
data() {
return {token: ''}
},
moment,vuetify,
router, store,
el: '#app',
methods: {
setToken(token)
{
this.token = token;
},
getToken()
{
return this.token;
}
}
});
A getter and setter available for the whole app to access. I may restrict access to the setter function later on but for now I am happy it works. Suggestions on restricting this will be nice.
Its now accessible via: this.$root.getToken()

Related

How to acced to google object in vue 2?

I'm trying to use the google maps API at my vue2 project and I have tried some ways that have failed. After using the vue2googlemaps module and the node module from google I have decided to use the CDN directly and add it to the index page. My problem now is that to acced to the google object, for example, to create a Marker or something like that, I need to use this.marker = new window.google.maps.Marker() for example, but in the tutorials I have seen, everyone uses directly the google object and never uses that window. I can`t understand why it happens. It would be appreciated if someone shows me the correct way to import or use this library on google.
It's because your template's code is compiled and executed in your component instance (a.k.a vm) 's scope, not in the global (a.k.a. window) scope.
To use google directly in your template you could add the following computed:
computed: {
google: () => window.google
}
If your problem is not having google defined in the component's <script>, a simple solution is to add it as a const at the top:
import Vue from 'vue';
const google = window.google;
export default Vue.extend({
computed: {
google: () => google // also make it available in template, as `google`
}
})
An even more elegant solution is to teach webpack to get google from the window object whenever it's imported in any of your components:
vue.config.js:
module.exports = {
configureWebpack: {
externals: {
google: 'window.google'
}
}
}
This creates a google namespace in your webpack configuration so you can import from it in any of your components:
import google from 'google';
//...
computed: {
google: () => google // provide it to template, as `google`
}
Why do I say it's more elegant?
Because it decouples the component from the context and now you don't need to modify the component when used in different contexts (i.e: in a testing environment, which might not even use a browser, so it might not have a window object, but a global instead; all you'd have to do in this case is to define a google namespace in that environment and that's where your component will get its google object from; but you wouldn't have to tweak or mock any of the component's methods/properties).

Vue 3 composition API, how to get context parent property in the setup() function?

I'm running into an issue with Vue 3 (alpha 4):
Inside the setup() function I am trying to read the parent component. As per the documentation on https://vue-composition-api-rfc.netlify.com/api.html#setup it should expose the parent via the context argument, either as a property of context.attrs or directly as parent (see the SetupContext bit under 'typing'). I don't find the documentation to be very clear on whether parent should be accessed directly from SetupContext, or via SetupContext.attrs, so I've tried both ways, but to no avail.
Here's my issue, I can access the SetupContext and SetupContext.attrs (which is a Proxy) just fine when logging them. SetupContext.attrs exposes the usual proxy properties ([[Handler]], [[Target]] and [[IsRevoked]]) and when inspecting [[Target]] it clearly shows the parent property.
When logging the parent though, it just prints out undefined:
export default {
setup(props, context) {
console.log(context);
// Output: {attrs: Proxy, slots: Proxy, emit: ƒ}
console.log(context.attrs);
// Output: Proxy {vnode: {…}, parent: {…}, appContext: {…}, type: {…}, root: {…}, …}
console.log(context.attrs.parent);
// Output: undefined
}
};
Spreading the context yields the same result:
export default {
setup(props, { attrs, parent }) {
console.log(attrs);
// Output: Proxy {vnode: {…}, parent: {…}, appContext: {…}, type: {…}, root: {…}, …}
console.log(attrs.parent);
// Output: undefined
console.log(parent);
// Output: undefined
}
};
I'm a bit new to proxies in JavaScript, but from what I've read on them, and from experimenting with proxies returned by reactive() for example. I should just be able to access the property like I normally would with an object. Any ideas on what I'm doing wrong?
I've created a codesandbox to reproduce the problem
You can use getCurrentInstance. It is undocumented Vue feature.
Usage is as easy as:
import { getCurrentInstance } from "vue";
export default {
setup(props) {
const instance = getCurrentInstance();
console.log("parent:");
console.log(instance.parent);
}
}
Vue considers it an internal api and warns against using it. For your consideration, you can read this github issue and this documentation from wayback machine.
Also, probably worth noting that Vue composition api plugin exposes parent in the same way, but it is referenced as instance.$parent there.
I know this doesn't answer the question directly, but using provide/inject (https://v3.vuejs.org/guide/component-provide-inject.html) has helped me resolve this same issue where I wanted to get a data attribute from the parent node and to pass it to the rendered component, but could not access the parent anymore after upgrading from Vue2 to Vue3. Rather than trying to expose the parent, I passed a prop from its dataset down to the rendered component.
Upon creating the app, I did the following.
main.js
import { createApp } from "vue";
import MyComponent from './components/MyComponent.vue';
const section = document.getElementById('some-element'); // this element contains a data-attribute which I need to use within the app. In my case, I can't get the data from the component created/mounted function as the section with the data attribute is loaded onto the page multiple times with different attributes each time.
const app = createApp(MyComponent);
app.provide('dataset', section.dataset.id); // section.dataset.id contains some kind of id, let's say 'abc123' for the sake of this example
app.use(store); //not relevant for this answer
app.mount(section);
Then, inside the component, I could access the 'dataset' by doing the following.
MyComponent.vue
<template>
<div>Normal template stuff here</div>
</template>
<script>
export default {
name: 'MyComponent',
inject: ['dataset'], // the magic
created() {
console.log(this.dataset); // will log 'abc123'
}
}
</script>
This is very stripped down, but shows my case nicely i guess. In any case, if you're trying to do something similar and want to get data via parent data attribute, you could look into provide/inject.
Hope it helps anyone out there!

Shared data from root instance

In my root Vue instance I am defining an isAdmin() method that does an ajax request to an API endpoint and determines if the user is an administrator or not, I put this method there as a lot of child components need to know if the user is an administrator or not.
The issue that I am running in to is that when the component accesses the root element data isAdmin the value is still false (default) and since the property is in the root instance the component does not react to the change.
Which is the best way to tackle what I'm tryin to achieve? I am trying to avoid having each component making it's own API call to the same endpoint.
Root instance:
window.App = new Vue({
el: '#app',
data: {
isAdmin: false
},
created()
{
this.checkIfAdmin()
},
methods: {
/**
* Check if the logged in User is an administrator.
*/
checkIfAdmin()
{
axios.get('/api/auth/isAdmin')
.then(({data}) => this.isAdmin = data)
}
}
});
And then on a children component:
mounted()
{
if (this.$root.$data.isAdmin) {
//Does not fire as `isAdmin` is still false at the time of parsing
console.log('I am an admin!')
}
},
Some of my components are not direct child.. ie.. grandchild.
this.$parent.isAdmin should give you the desired result.
Forgot to mention, some of my components are not direct child.. ie.. grandchild.
Ah, yes, you can use this.$root.isAdmin then.

VueJS - Define components in secondary .js file

I have this situation: I have an Laravel app that is not an SPA. But I use Vue in many components of my application, using single-file components and all of that.
But I have some components that I only want to load in some pages, for performance reasons. But, when I register a component in a second .js file, I always get an error like this:
Property or method "addresses" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
(found in )
My structure is something like this:
app.js
window.Vue = require('vue');
import ModalMessage from './../components/ModalMessage.vue';
import FlashMessage from './../components/FlashMessage.vue';
window.MainVue = new Vue({
el: '#app',
data: {
loading: false
},
components: { ModalMessage, FlashMessage }
});
All of this work's perfectly fine. But when I introduce a new .js file, I always get the error mentioned above.
Example:
account.js
window.UserAddresses = new Vue({
el: '#user-addresses',
data: {
addresses: []
},
methods: {
}
});
The HTML for the #user-addresses is:
<div id="user-addresses">
<table class="table">
<tr v-for="address in addresses">
<td>testing</td>
</tr>
</table>
</div>
And I always get the error:
Property or method "addresses" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
(found in )
In this internal page that generates the error, my JS files are included like this:
<script src="assets/js/app.js"></script>
<script src="assets/js/account.js"></script>
The account.js Vue instance is mounted before generates the error.
My question is: Is there a way to work with Vue in multiple .js files? I also didn't want to declare all my subcomponents data in my root Vue instance.
Is there a way to make this work?
UPDATE
I'm using webpack for compiling all files.
In general your top example looks just right and nothing I would see out of order. The problem though you describe with the addresses probably comes from how you use the UserAddresses.
I would guess you have it nested inside the #app element on the page.
The problem here is, you can't nest two Vue root instances in each other.
new Vue({...}) // get a Vue rootinstance
What you probably want to do is use a Component. You can use global components for this, and therefor it's fine to have one global root instance.
window.UserAddresses = Vue.component('user-addresses', {
data () {
return {
addresses: []
}
}
... // rest of your component code
}
You can get more information here: https://v2.vuejs.org/v2/guide/components.html

Vue-multiselect inconsistent reactive options

So I'm building an application using Laravel Spark, and therefore taking the opportunity to learn some Vue.js while I'm at it.
It's taken longer for me to get my head around it than I would have liked but I have nearly got Vue-multiselect working for a group of options, the selected options of which are retrieved via a get request and then updated.
The way in which I've got this far may well be far from the best, so bear with me, but it only seems to load the selected options ~60% of the time. To be clear - there are never any warnings/errors logged in the console, and if I check the network tab the requests to get the Tutor's instruments are always successfully returning the same result...
I've declared a global array ready:
var vm = new Vue({
data: {
tutorinstruments: []
}
});
My main component then makes the request and updates the variable:
getTutor() {
this.$http.get('/get/tutor')
.then(response => {
this.tutor = response.data;
this.updateTutor();
});
},
updateTutor() {
this.updateTutorProfileForm.profile = this.tutor.profile;
vm.tutorinstruments = this.tutor.instruments;
},
My custom multiselect from Vue-multiselect then fetches all available instruments and updates the available instruments, and those that are selected:
getInstruments() {
this.$http.get('/get/instruments')
.then(response => {
this.instruments = response.data;
this.updateInstruments();
});
},
updateInstruments() {
this.options = this.instruments;
this.selected = vm.tutorinstruments;
},
The available options are always there.
Here's a YouTube link to how it looks if you refresh the page over and over
I'm open to any suggestions and welcome some help please!
Your global array var vm = new Vue({...}) is a separate Vue instance, which lives outside your main Vue instance that handles the user interface.
This is the reason you are using both this and vm in your components. In your methods, this points to the Vue instance that handles the user interface, while vm points to your global array that you initialized outside the Vue instance.
Please check this guide page once more: https://v2.vuejs.org/v2/guide/instance.html
If you look at the lifecycle diagram that initializes all the Vue features, you will notice that it mentions Vue instance in a lot of places. These features (reactivity, data binding, etc.) are designed to operate within a Vue instance, and not across multiple instances. It may work once in a while when the timing is right, but not guaranteed to work.
To resolve this issue, you can redesign your app to have a single Vue instance to handle the user interface and also data.
Ideally I would expect your tutorinstruments to be loaded in a code that initializes your app (using mounted hook in the root component), and get stored in a Vuex state. Once you have the data in your Vuex state, it can be accessed by all the components.
Vuex ref: https://vuex.vuejs.org/en/intro.html
Hope it helps! I understand I haven't given you a direct solution to your question. Maybe we can wait for a more direct answer if you are not able to restructure your app into a single Vue instance.
What Mani wrote is 100% correct, the reason I'm going to chime in is because I just got done building a very large scale project with PHP and Vue and I feel like I'm in a good position to give you some advice / things I learned in the process of building out a PHP (server side) website but adding in Vue (client side) to the mix for the front end templating.
This may be a bit larger than the scope of your multiselect question, but I'll give you a solid start on that as well.
First you need to decide which one of them is going to be doing the routing (when users come to a page who is handling the traffic) in your web app because that will determine the way you want to go about using Vue. Let's say for the sake of discussion you decide to authenticate (if you have logins) with PHP but your going to handle the routing with Vue on the front end. In this instance your going to want to for sure have one main Vue instance and more or less set up something similar to this example from Vue Router pretending that the HTML file is your PHP index.php in the web root, this should end up being the only .php file you need as far as templating goes and I had it handle all of the header meta and footer copyright stuff, in the body you basically just want one div with the ID app.
Then you just use the vue router and the routes to load in your vue components (one for each page or category of page works easily) for all your pages. Bonus points if you look up and figure using a dynamic component in your main app.vue to lazy load in the page component based on the route so your bundle stays small.
*hint you also need a polyfill with babel to do this
template
<Component :is="dynamicComponent"/>
script
components: {
Account: () => import('./Account/Account.vue'),
FourOhFour: () => import('../FourOhFour.vue')
},
computed: {
dynamicComponent() {
return this.$route.name;
}
},
Now that we are here we can deal with your multiselect issue (this also basically will help you to understand an easy way to load any component for Vue you find online into your site). In one of your page components you load when someone visits a route lets say /tutor (also I went and passed my authentication information from PHP into my routes by localizing it then using props, meta fields, and router guards, its all in that documention so I'll leave that to you if you want to explore) on tutor.vue we will call that your page component is where you want to call in multiselect. Also at this point we are still connected to our main Vue instance so if you want to reference it or your router from tutor.vue you can just use the Vue API for almost anything subbing out Vue or vm for this. But the neat thing is in your main JS file / modules you add to it outside Vue you can still use the API to reference your main Vue instance with Vue after you have loaded the main instance and do whatever you want just like you were inside a component more or less.
This is the way I would handle adding in external components from this point, wrapping them in another component you control and making them a child of your page component. Here is a very simple example with multiselect pretend the parent is tutor.vue.
Also I have a global event bus running, thought you might like the idea
https://alligator.io/vuejs/global-event-bus/
tutor.vue
<template>
<div
id="user-profile"
class="account-content container m-top m-bottom"
>
<select-input
:saved-value="musicPreviouslySelected"
:options="musicTypeOptions"
:placeholder="'Choose an your music thing...'"
#selected="musicThingChanged($event)"
/>
</div>
</template>
<script>
import SelectInput from './SelectInput';
import EventBus from './lib/eventBus';
export default {
components: {
SelectInput
},
data() {
return {
profileLoading: true,
isFullPage: false,
isModalActive: false,
slackId: null,
isActive: false,
isAdmin: false,
rep: {
id: null,
status: '',
started: '',
email: '',
first_name: '',
},
musicTypeOptions: []
};
},
created() {
if (org.admin) {
this.isAdmin = true;
}
this.rep.id = parseInt(this.$route.params.id);
this.fetchData();
},
mounted() {
EventBus.$on('profile-changed', () => {
// Do something because something happened somewhere else client side.
});
},
methods: {
fetchData() {
// use axios or whatever to fetch some data from the server and PHP to
// load into the page component so say we are getting the musicTypeOptions
// which will be in our selectbox.
},
musicThingChanged(event) {
// We have our new selection "event" from multiselect so do something
}
}
};
</script>
this is our child Multiselect wrapper SelectInput.vue
<template>
<multiselect
v-model="value"
:options="options"
:placeholder="placeholder"
label="label"
track-by="value"
#input="inputChanged" />
</template>
<script>
import Multiselect from 'vue-multiselect';
export default {
components: { Multiselect },
props: {
options: {
type: [Array],
default() {
return [];
}
},
savedValue: {
type: [Array],
default() {
return [];
}
},
placeholder: {
type: [String],
default: 'Select Option...'
}
},
data() {
return {
value: null
};
},
mounted() {
this.value = this.savedValue;
},
methods: {
inputChanged(selected) {
this.$emit('selected', selected.value);
}
}
};
</script>
<style scoped>
#import '../../../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
</style>
Now you can insure you are manging the lifecycle of your page and what data you have when, you can wait until you get musicTypeOptions before it will be passed to SelectInput component which will in turn set up Multiselect or any other component and then handle passing the data back via this.$emit('hihiwhatever') which gets picked up by #hihiwhatever on the component in the template which calls back to a function and now you are on your way to do whatever with the new selection and pass different data to SelectInput and MultiSelect will stay in sync always.
Now for my last advice, from experience. Resist the temptation because you read about it 650 times a day and it seems like the right thing to do and use Vuex in a setup like this. You have PHP and a database already, use it just like Vuex would be used if you were making is in Node.js, which you are not you have a perfectly awesome PHP server side storage, trying to manage data in Vuex on the front end, while also having data managed by PHP and database server side is going to end in disaster as soon as you start having multiple users logged in messing with the Vuex data, which came from PHP server side you will not be able to keep a single point of truth. If you don't have a server side DB yes Vuex it up, but save yourself a headache and wait to try it until you are using Node.js 100%.
If you want to manage some data client side longer than the lifecycle of a page view use something like https://github.com/gruns/ImmortalDB it has served me very well.
Sorry this turned into a blog post haha, but I hope it helps someone save themselves a few weeks.