I have created a simple plugin for my Vue2.js project to get some lists for my select fields in forms. The plugin should make some axios call and return the response.
const ListSelector = {
install (Vue){
Vue.mixin({
methods: {
getSubscriberType: function() {
this.$http
.get('/web/admin/contract/subscribers')
.then(function(response) { return response.data })
},
//other methods (they do not work the same...)...
}
})
}
}
export default ListSelector
I registered the plugin in my main.js
import ListSelector from './backend/selectable'
Vue.use(ListSelector)
Now, if i try to call the plugin method in a component, i get that is undefined
<template>
...
<b-form-select v-model="form.type" :options="options" id="type" required></b-form-select>
...
</template>
<script>
export default {
data(){
return {
options: {}
}
},
mounted(){
this.options = this.getSubscriberType()
}
}
<script>
I get that this.getSubscriberType() is undefined
EDIT: i actually see that the function is fired (i put an alert in it... but if i do a console.log(this.getSubscriberType()) in the component, i get undefined...
It looks like your method never actually returns anything. $http.get() returns a promise, and promises are by their nature asyncronous.
const ListSelector = {
install (Vue){
Vue.mixin({
methods: {
getSubscriberType: function() {
this.$http
.get('/web/admin/contract/subscribers') //returns a new promise
.then(function(response) { //Executes after the promise resolves
return response.data //not in the same scope as getSubscriberType
})
// getSubscriberType completes without a return value, hence undefined
}
}
})
}
}
Because the function resolves before the promise is resolved, the getSubscriberType does not even have the response available to it.
Instead, you want to return the promise.
getSubscriberType: function() {
return this.$http.get('/web/admin/contract/subscribers') // now returns promise
}
Then you would do whatever you need to do with the response locally when it completes
var self = this // gives you a way to access local scope when the promise resolves
getSubscriberType().then(funciton(response) {
self.subscriberType = response.data
})
In this example, it would set the subscriberType value on the component when the http call completes successfully.
Related
I'm working with some objects (classes) in my TS codebase which perform async operations right after their creation. While everything is working perfectly fine with Vue 2.x (code sample), reactivity breaks with Vue3 (sample) without any errors.
The examples are written in JS for the sake of simplicity, but behave the same as my real project in TS.
import { reactive } from "vue";
class AsyncData {
static Create(promise) {
const instance = new AsyncData(promise, false);
instance.awaitPromise();
return instance;
}
constructor(promise, immediate = true) {
// working, but I'd like to avoid using this
// in plain TS/JS object
// this.state = reactive({
// result: null,
// loading: true,
// });
this.result = null;
this.loading = true;
this.promise = promise;
if (immediate) {
this.awaitPromise();
}
}
async awaitPromise() {
const result = await this.promise;
this.result = result;
this.loading = false;
// this.state.loading = false;
// this.state.result = result;
}
}
const loadStuff = async () => {
return new Promise((resolve) => {
setTimeout(() => resolve("stuff"), 2000);
});
};
export default {
name: "App",
data: () => ({
asyncData: null,
}),
created() {
// awaiting promise right in constructor --- not working
this.asyncData = new AsyncData(loadStuff());
// awaiting promise in factory function
// after instance creation -- not working
// this.asyncData = AsyncData.Create(loadStuff());
// calling await in component -- working
// this.asyncData = new AsyncData(loadStuff(), false);
// this.asyncData.awaitPromise();
},
methods: {
setAsyncDataResult() {
this.asyncData.loading = false;
this.asyncData.result = "Manual data";
},
},
};
<div id="app">
<h3>With async data</h3>
<button #click="setAsyncDataResult">Set result manually</button>
<div>
<template v-if="asyncData.loading">Loading...</template>
<template v-else>{{ asyncData.result }}</template>
</div>
</div>
The interesting part is, that the reactivity of the object seems to be completely lost if an async operation is called during its creation.
My samples include:
A simple class, performing an async operation in the constructor or in a factory function on creation.
A Vue app, which should display "Loading..." while the operation is pending, and the result of the operation once it's finished.
A button to set the loading flag to false, and the result to a static value manually
parts commented out to present the other approaches
Observations:
If the promise is awaited in the class itself (constructor or factory function), the reactivity of the instance breaks completely, even if you're setting the data manually (by using the button)
The call to awaitPromise happens in the Vue component everything is fine.
An alternative solution I'd like to avoid: If the state of the AsyncData (loading, result) is wrapped in reactive() everything works fine with all 3 approaches, but I'd prefer to avoid mixing Vue's reactivity into plain objects outside of the view layer of the app.
Please let me know your ideas/explanations, I'm really eager to find out what's going on :)
EDIT: I created another reproduction link, which the same issue, but with a minimal setup: here
I visited the code sample you posted and it it is working, I observed this:
You have a vue component that instantiates an object on its create hook.
The instantiated object has an internal state
You use that state in the vue component to render something.
it looks something like this:
<template>
<main>
<div v-if="myObject.internalState.loading"/>
loading
</div>
<div v-else>
not loading {{myObject.internalState.data}}
</div>
</main>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
data(){
return {
myObject:null
}
},
created(){
this.myObject = new ObjectWithInternalState()
},
});
</script>
ObjectWithInternalState is doing an async operation when instantiated and changing its internalState but when internalState is a plain object then nothing is reactive. This is the expected behavior since changing any internal value of internalState is not a mutation on myObject (vue reactive value), but if instead of using a plain object for internalState yo use a reactive object (using the composition API) and since you are accessing that value on the template then all the changes made to that object are observed by the template (reactivity!!). If you don't want to have mixed things then you need to wait for the async operation in the component.
export default defineComponent({
name: 'App',
data(){
return {
remoteData:null,
loading:false
}
},
created(){
this.loading = true
// Option 1: Wait for the promise (could be also async/await
new ObjectWithInternalState().promise
.then((result)=>{
this.loading = false
this.remoteData = result
})
// Option 2: A callback
new ObjectWithInternalState(this.asyncFinished.bind(this))
},
methods:{
asyncFinished(result){
this.loading = false
this.remoteData = result
}
}
});
My recommendation is to move all state management to a store, take a look at Vuex It is the best practice for what are you intending
Szia Ćbel,
I think the problem you're seeing might be due to the fact that Vue 3 handles the reactivity differently. In Vue2, the values sent were sort of decorated with additional functionality, whereas in Vue 3, reactivty is done with Proxy objects. As a result, if you do a this.asyncData = new AsyncData(loadStuff());, Vue 3 may replace your reactive object with the response of new AsyncData(loadStuff()) which may loose the reactivity.
You could try using a nested property like
data: () => ({
asyncData: {value : null},
}),
created() {
this.asyncData.value = new AsyncData(loadStuff());
}
This way you're not replacing the object. Although this seems more complicated, by using Proxies, Vue 3 can get better performance, but loses IE11 compatibility.
If you want to validate the š hypothesis, you can use isReactive(this.asyncData) before and after you make the assignment. In some cases the assignment works without losing reactivity, I haven't checked with the new Class.
Here's an alternate solution that doesn't put reactive into your class
created() {
let instance = new AsyncData(loadStuff());
instance.promise.then((r)=>{
this.asyncData = {
instance: instance,
result: this.asyncData.result,
loading: this.asyncData.loading,
}
});
this.asyncData = instance;
// or better yet...
this.asyncData = {
result: instance.result,
loading: instance.loading
};
}
But it's not very elegant. It might be better to make the state an object you pass to the class, which should work for vue and non-vue scenarios.
Here's what that might look like
class withAsyncData {
static Create(state, promise) {
const instance = new withAsyncData(state, promise, false);
instance.awaitPromise();
return instance;
}
constructor(state, promise, immediate = true) {
this.state = state || {};
this.state.result = null;
this.state.loading = true;
this.promise = promise;
if (immediate) {
this.awaitPromise();
}
}
async awaitPromise() {
const result = await this.promise;
this.state.result = result;
this.state.loading = false;
}
}
const loadStuff = async () => {
return new Promise((resolve) => {
setTimeout(() => resolve("stuff"), 2000);
});
};
var app = Vue.createApp({
data: () => ({
asyncData: {},
}),
created() {
new withAsyncData(this.asyncData, loadStuff());
// withAsyncData.Create(this.asyncData, loadStuff());
// let instance = new withAsyncData(this.asyncData, loadStuff(), false);
// instance.awaitPromise();
},
methods: {
setAsyncDataResult() {
this.asyncData.loading = false;
this.asyncData.result = "Manual data";
},
},
});
app.mount("#app");
<script src="https://unpkg.com/vue#3.0.11/dist/vue.global.prod.js"></script>
<div id="app">
<div>
<h3>With async data</h3>
<button #click="setAsyncDataResult">Set result manually</button>
<div>
<template v-if="asyncData.loading">Loading...</template>
<template v-else>{{ asyncData.result }}</template>
</div>
</div>
</div>
I am trying to separate my axios calls from my main vue instance by importing them instead of calling them directly in the created hook.
I have this in a separate file called data.js
import axios from 'axios'
export default{
myData() {
return axios.get(`http://localhost:8080/data.json`)
.then(response => {
// JSON responses are automatically parsed.
return response.data;
})
.catch(e => {
return this.myErrors.push(e)
});
},
And in my vue instance I have the following:
import myDataApi from '#/api/data.js'
export default {
name: 'app',
components: {
myDataApi, // not sure if this is correct
},
data: function () {
return {
myInfo: '',
}
},
created() {
this.myInfo = myDataApi.myData();
console.log('this.myInfo= ', this.myInfo)
},
I am trying to populate myInfo with the json called by myData. This returns [object Promise] in Vue devtools and the as PromiseĀ {<pending>} in the console.
All the data I need is inside that PromiseĀ {<pending>} in an array called [[PromiseValue]]:Object so I know it is working, I just need to know the correct way implementing this.
I don't have a development environment enabled to test this at the moment, but I do notice that you are trying to assign a variable the moment that the component is initialized. This object is a promise, but you're not handling the promise after it is resolved inside the component where you have imported it.
I would recommend trying to handle the promise inside of the actual component, something like:
import myDataApi from '#/api/data.js'
export default {
name: 'app',
components: {
myDataApi, // not sure if this is correct
},
data: function () {
return {
myInfo: '',
}
},
created() {
myDataApi.myData()
.then((data) => {
this.myInfo = data
console.log('this.myInfo= ', this.myInfo);
});
.catch((e) => handleError) // however you want to handle it
},
Just to add to #LexJacobs answer. I omitted the parenthesis around data in .then() as seen below. Vue was squawking about data not being available even though it was. This solved that problem, although to be honest I don't know why.
myDataApi.myData()
.then(data => {
this.dataHasLoaded = true;
this.myInfo = data;
})
.catch(e => {
this.myErrors.push(e)
});
I have a function that returns an object with many methods and I need to check one of the methods inside this returned object. I am using AngularJS and Karma+Jasmine as testing suite. How do I call methods inside the object returned by a function?
function modalOptions() {
.........
return this.$q((resolve) => {
// test accessable here
this.solveModel = {
save: () => {
// test can't call save()
this.saveToDB = this.toSendToDB;
},
cancel: () => { ...
},
delete: () => { ...
}
};
});
}
My test is somewhat like this...
it('should save modal with the data', function() {
scope.$apply();
expect(vm.modalOptions).toBeDefined();
vm.toSendToDB = true; // hard-coded
vm.savedToDB = undefined // default value from other controller
spyOn(vm, 'modalOptions').and.callThrough();
console.log(vm.modalOptions()); // gives weird response: c{$$state: Object{status: 0}} instead of the solveModal object
expect(vm.toSendToDB).toBeTruthy();
expect(vm.savedToDB).toBeTruthy();
});
Sorry, I can not comment yet, but the promise has to be resolved and the solveModel passed to it, in order for solveModel to be returned. Where do you resolve the promise?
I am building an Angular 1.5 app using the component structure. After the promise comes back from the $http call in the service, I am trying to call another function to filter the dataset before it is displayed on the UI.
However, the filterApps function is not getting called.
Also...in the filterApps function I am trying to compare to arrays of objects and return back the ones that have the same name. Is this the best way to go about this or is there a cleaner way?
Controller :
import allApps from '../../resources/data/application_data.js';
class HomeController {
/*#ngInject*/
constructor(ItemsService) {
this.itemsService = ItemsService;
this.displayApps = [];
}
$onInit() {
this.itemsService
.getItems()
.success((apps) => this.filterApps(apps));
}
filterApps(siteApps) {
this.displayApps = allApps.applications.filter((app) => {
siteApps.applications.map((siteApp) => {
if(siteApp.name === app.name) {
return app;
}
})
});
}
}
export default HomeController;
I don't see any reason that filterApps method isn't geting call(as you already commented that success function is getting called). I guess you're just checking nothing has been carried in displayApps variable. The real problem is you have not return internal map function result to filter. So that's why nothing gets return.
Code
filterApps(siteApps) {
this.displayApps = allApps.applications.filter((app) => {
//returning map function result.
return siteApps.applications.map((siteApp) => {
if(siteApp.name === app.name) {
return app;
}
})
});
}
I have pass two callback function success and error on promise returned from ajax call using then method. Now i am unable to get Ember component object inside success/error method.
import Ember from 'ember';
export default Ember.Component.extend({
data:null,
issueType:'',
description:null,
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
this.set('ticket.category',data.category);
this.set('ticket.name',this.get('session.currentUser.first_name'));
this.set('ticket.phone',this.get('session.currentUser.phone'));
this.set('ticket.groupId',data.groupId);
this.set('ticket.ownerId',this.get('session.currentUser.id'));
this.set('ticket.oyoId',this.get('session.currentOwnerHotelOyoId'));
this.set('ticket.ticketRaisedBy','owner');
this.set('ticket.bookingId',data.bookingId);
this.set('ticket.subType',data.subType);
this.set('ticket.subSubIssue',data.subSubIssue);
this.set('ticket.email',this.get('ticket.oyoId')+'#oyoproperties.com');
this.set('ticket.subject',this.get('ticket.oyoId')+' : '+this.get('ticket.category'));
this.set('ticket.description',this.get('description'));
},
success:function(){
console.log(this.get('description'));
},
error:function(){
console.log(this.get('description'));
},
actions :{
submitIssue:function(){
this.prepareSubmitRaiseIssueModal();
this.get('ticket').submitRaiseIssue().then(this.success,this.error);
//this.send('closeRaiseIssueModal');
},
closeRaiseIssueModal:function(){
this.sendAction('closeRaiseIssueModal');
}
}
});
i am able to get Ember component object if instead of passing named function i pass anonymous function.
submitIssue:function(){
var self = this;
this.prepareSubmitRaiseIssueModal();
this.get('ticket').submitRaiseIssue().then(function(response){
console.log(self.get('description'));
},
function(err){
console.log(self.get('description'));
});
//this.send('closeRaiseIssueModal');
},
is there any way i can get the Ember component object's reference for former case??
Wow speaking of a spaghetti.
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
this.set('ticket.category',data.category);
this.set('ticket.name',this.get('session.currentUser.first_name'));
this.set('ticket.phone',this.get('session.currentUser.phone'));
this.set('ticket.groupId',data.groupId);
this.set('ticket.ownerId',this.get('session.currentUser.id'));
this.set('ticket.oyoId',this.get('session.currentOwnerHotelOyoId'));
this.set('ticket.ticketRaisedBy','owner');
this.set('ticket.bookingId',data.bookingId);
this.set('ticket.subType',data.subType);
this.set('ticket.subSubIssue',data.subSubIssue);
this.set('ticket.email',this.get('ticket.oyoId')+'#oyoproperties.com');
this.set('ticket.subject',this.get('ticket.oyoId')+' : '+this.get('ticket.category'));
this.set('ticket.description',this.get('description'));
},
How about
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
var ticket = this.get('ticket')
ticket.setProperties({
'category': data.category,
'name': ...
})
},
And to pass reference's you can either use
promise.then(function() {
this.mysuccess();
}.bind(this), function() {
this.myerror();
}.bind(this))
const self = this;
promise.then(function() {
self.blah();
});
promise.then(result => {Ā
this.blah();
})Ā
In your case I would write a utility JS file for displaying notifications.
And handle success for each promise personally and let errors be handled in a general error method.
utils/notifications.js
function reportError(error) {
displayNotification('error', getErrorMessage(error));
}
import reportError from 'utils/notifications';
export default Ember.Controller.extend({
....
promise.then(result => {
// Do custom stuff with result;
}, reportError);
....
});
And promise within a promise
return promise1.then(x => {
return promise2.then(x2 => {
return promise3 ... etc
})
}).catch(reportError); // Single hook needed but u need to return promises