I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.
Related
I have a tooltip control I've written that works very nicely in Vue 3, but I need a mechanism to fire off to all other instances to tell them to close. There are delays on close, so I'm occasionally getting two tooltips to show up at the same time.
This method, which was a crutch I've used in the past, is not allowed by the compiler / build tools. I can full well understand why, but I don't know the right way:
tooltipManager: function() {
if (!window.TooltipManager) {
function tooltipManager() {
let _data = {
tooltipIndex: 0,
callbacks: {}
};
return {
register: function (callback) {
let id = "tooltip_" + _data.tooltipIndex;
_data.tooltipIndex++;
_data.callbacks[id] = callback;
return id;
},
closeOpenPopups: function (id) {
Object.keys(_data.callbacks).forEach(key => {
if (id !== key) {
_data.callbacks[key]();
}
});
},
destroy: function (id) {
delete _data.callbacks[id];
}
};
}
window.TooltipManager = tooltipManager();
}
return window.TooltipManager()
},
The first thing I tried but didn't work was a service which I imported:
export default class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
Ok, I was close with the first service. It should be written this way, and I'm going to leave my console.logs in that confirmed that it is indeed a singleton even though it is running on different tooltips.
class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
console.log("got new instance");
} else {
console.log("got old instance");
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
console.log("registered key: " + id);
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
console.log("closed: " + key);
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
export default new TooltipManager();
I got the following from console.logs:
got new instance
TooltipManager.js:19 registered key: tooltip_0
TooltipManager.js:19 registered key: tooltip_1
TooltipManager.js:19 registered key: tooltip_2
TooltipManager.js:19 registered key: tooltip_3
TooltipManager.js:26 closed: tooltip_0
TooltipManager.js:26 closed: tooltip_1
TooltipManager.js:26 closed: tooltip_2
TooltipManager.js:26 closed: tooltip_3
And indeed it solved the problem of ghost tooltips when one pops up before the other closes with a delay to prevent bounce.
In the tooltip tool I wrote, which I will later post here as an example of how easy Vue3 Teleport makes something like this to write. I want to test it a little longer before I show it off.
I just need to:
mounted() {
this.data.tooltipId = TooltipManager.register(this.forceHide);
And, which also shows some state data I use to keep track of this:
methods: {
forceHide: function() {
if (this.data.isDisplayed) {
this.data.style = '{top: -1000px, left: -1000px}';
}
this.data.hideRequested = false;
this.data.showRequested = false;
this.data.isDisplayed = false;
},
Now the next thing maybe using Vuex for this, but I may leave this in as an alternative method so it's not dependent on it.
I'm building a little vue.js-application where I do some post requests. I use the watch-method to whach for api changes which then updates the component if the post request is successfull. Since the watcher constantly checks the API I want to add the ._debounce method but for some reason it doesn't work.
here is the code:
<script>
import _ from 'lodash'
export default {
data () {
return {
cds: [],
cdCount: ''
}
},
watch: {
cds() {
this.fetchAll()
}
},
methods: {
fetchAll: _.debounce(() => {
this.$http.get('/api/cds')
.then(response => {
this.cds = response.body
this.cdCount = response.body.length
})
})
},
created() {
this.fetchAll();
}
}
</script>
this gives me the error: Cannot read property 'get' of undefined
Can someone maybe tell me what I'm doing wrong?
EDIT
I removed the watch-method and tried to add
updated(): {
this.fetchAll()
}
with the result that the request runs in a loop :-/ When I remove the updated-lifecycle, the component does (of course) not react to api/array changes... I'm pretty clueless
Mind the this: () => { in methods make the this reference window and not the Vue instance.
Declare using a regular function:
methods: {
fetchAll: _.debounce(function () {
this.$http.get('/api/cds/add').then(response => {
this.cds = response.body
this.cdCount = response.body.length
})
})
},
Other problems
You have a cyclic dependency.
The fetchAll method is mutating the cds property (line this.cds = response.body) and the cds() watch is calling this.fetchAll(). As you can see, this leads to an infinite loop.
Solution: Stop the cycle by removing the fetchAll call from the watcher:
watch: {
cds() {
// this.fetchAll() // remove this
}
},
I have been stuck with this issues for 2 hours now and I really can't seem to get it work.
const app = new Vue({
el: '#book-search',
data: {
searchInput: 'a',
books: {},
},
methods: {
foo: function () {
axios.get('https://www.googleapis.com/books/v1/volumes', {
params: {
q: this.searchInput
}
})
.then(function (response) {
var items = response.data.items
for (i = 0; i < items.length; i++) {
var item = items[i].volumeInfo;
Vue.set(this.books[i], 'title', item.title);
}
})
.catch(function (error) {
console.log(error);
});
}
}
});
When I initiate search and the API call I want the values to be passed to data so the final structure looks similar to the one below.
data: {
searchInput: '',
books: {
"0": {
title: "Book 1"
},
"1": {
title: "Book 2"
}
},
Currently I get Cannot read property '0' of undefined.
Problem lies here:
Vue.set(this.books[i], 'title', item.title);
You are inside the callback context and the value of this is not the Vue object as you might expect it to be. One way to solve this is to save the value of this beforehand and use it in the callback function.
Also instead of using Vue.set(), try updating the books object directly.
const app = new Vue({
el: '#book-search',
data: {
searchInput: 'a',
books: {},
},
methods: {
foo: function () {
var self = this;
//--^^^^^^^^^^^^ Save this
axios.get('https://www.googleapis.com/books/v1/volumes', {
params: {
q: self.searchInput
//-^^^^--- use self instead of this
}
})
.then(function (response) {
var items = response.data.items
var books = {};
for (i = 0; i < items.length; i++) {
var item = items[i].volumeInfo;
books[i] = { 'title' : item.title };
}
self.books = books;
})
.catch(function (error) {
console.log(error);
});
}
}
});
Or if you want to use Vue.set() then use this:
Vue.set(self.books, i, {
'title': item.title
});
Hope this helps.
yep, the problem is about context. "this" returns not what you expect it to return.
you can use
let self = this;
or you can use bind
function(){this.method}.bind(this);
the second method is better.
Also google something like "how to define context in js", "bind call apply js" - it will help you to understand what is going wrong.
// update component's data with some object's fields
// bad idea, use at your own risk
Object
.keys(patch)
.forEach(key => this.$data[key] = patch[key])
I have the following code:
{
data: function () {
return {
questions: [],
sendButtonDisable: false,
}
},
methods: {
postQuestionsContent: function () {
var that = this;
that.sendButtonDisable = true;
},
},
},
I need to change sendButtonDisable to true when postQuestionsContent() is called. I found only one way to do this; with var that = this;.
Is there a better solution?
Inside methods if you don't have another scope defined inside, you can access your data like that:
this.sendButtonDisable = true;
but if you have a scope inside the function then in vue is a common usage of a variable called vm (stands for view model) at the beginning of the function, and then just use it everywhere like:
vm.sendButtonDisable = false;
An example of vm can be seen in the Vue official documentation as well.
complete example:
data: function () {
return {
questions: [],
sendButtonDisable : false
}
},
methods: {
postQuestionsContent : function() {
// This works here.
this.sendButtonDisable = true;
// The view model.
var vm = this;
setTimeout(function() {
// This does not work, you need the outside context view model.
//this.sendButtonDisable = true;
// This works, since wm refers to your view model.
vm.sendButtonDisable = true;
}, 1000);
}
}
It depends on how you call your postQuestionsContent method (if you call it asynchronously, you might need to bind the this context).
In most cases, you should be able to access it using this.$data.YOURPROPNAME, in your case this.$data.sendButtonDisable:
data: function () {
return {
questions: [],
sendButtonDisable : false
}
},
methods:
{
postQuestionsContent : function()
{
this.$data.sendButtonDisable = true;
}
}
Try this instead
...
methods:
{
postQuestionsContent ()
{
this.sendButtonDisable = true;
}
}
Registering your methods in the above manner should resolve the issue.
I tried both this.$data.sendButtonDisable and vm.sendButtonDisable and did not work for me.
But I got it working with outer_this = this, something like:
methods: {
sendForm(){
var outer_this;
outer_this = this;
$.ajax({
url: "email.php",
type: "POST",
dataType: "text",
data: {
abc: "..."
},
success: function(res){
if(res){
//...
outer_this.sendButtonDisable = false;
}else{
//...
}
}
});
}
},
What's the best way to set state based on the data received from observe()?
It seems setting state via componentWillMount() won't work as observe() runs after this and the data isn't available to set state.
I'm using the observe function as advised when using Parse
E.g.:
var DragApp = React.createClass({
getInitialState: function () {
return {
activeCollection : ''
};
},
observe: function() {
return {
collections: (collectionsQuery.equalTo("createdBy", currentUser))
};
},
_setactiveCollection: function(collection) {
this.setState({
activeCollection : collection
});
},
componentWillMount: function () {
var collection = this.data.collections[0];
this._setActiveCollection(collection);
},
)}
I went the wrong way about this.
I shouldn't be storing this.data into state. I can pass it into components via render.
To get round this.data not being ready before rendering, I make use of the ParseReact function pendingQueries() inside render. E.g.
if (this.pendingQueries().length > 0) {
content = 'loading...'
} else {
content = 'hello world I am' + this.data.name
}
Try:
var DragApp = React.createClass({
observe: function() {
var collections = collectionsQuery.equalTo("createdBy", currentUser);
return {
collections: collections,
activeCollection: collections[0]
};
},
render: function () {
// do something with this.data.collections and/or this.data.activeCollection
},
)}