In a React app I need to post data from a form. The post creates a new dashboard object. Once that's done, I need to immediately update a select dropdown in the component to include the newly added dashboard name. The axios documentation says it should be done like so:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// Both requests are now complete
}));
So this is what I've done:
class DashboardForm extends Component {
saveDashboard() {
var siteId = this.state.siteId;
var self= this;
return axios.post('/path/to/save/dashboard' + siteId + '/dashboards', {
slug: this.refs.dashboardUrl.value,
name: this.refs.dashboardName.value,
}).then(function (response) {
self.setState({
dashboardId: response.data.dashboardId,
dashboardName: response.data.dashboardName,
submitMessage: (<p>Successfully Created</p>)
});
self.setUrl(siteId, response.data.dashboardId);
}).catch(function (error) {
console.log(error);
self.setState({
submitMessage: (<p>Failed</p>)
});
});
}
getAllDashboards(){
var self = this;
self.setState({siteId: this.props.selectedSiteID});
var getDashboardsPath = "path/to/get/dashboards/" + self.props.selectedSiteID + "/dashboards";
axios(getDashboardsPath, {
credentials: 'include',
method: 'GET',
cache: 'no-cache'
}).then(function (response) {
return response.data.dashboards;
}).then(function (arrDashboards) { //populate options for the select
var options = [];
for (var i = 0; i < arrDashboards.length; i++) {
var option = arrDashboards[i];
options.push(
(<option className="dashboardOptions" key={option.dashboardId} value={option.slug}>{option.name}</option>)
);
}
self.setState({
options: options
});
});
}
saveAndRepopulate() {
axios.all([saveDashboard(), getAllDashboards()])
.then(axios.spread(function (savedDashboard, listOfDashboards) {
// Both requests are now complete
}));
}
}
The saveAndRepopulate is called when the form submits.
The problem is that I get the following errors:
error 'saveDashboard' is not defined no-undef
error 'getAllDashboards' is not defined no-undef
I've tried doing
function saveDashboard() {
but then I get
Syntax error: Unexpected token, expected ( (175:13)
|
| function saveDashboard() {
| ^
How do I call these two functions? Also, am I going to need to change the promise (.then) from the individual calls to the saveAndPopulate?
Many thanks for any guidance.
First, as #Jaromanda X pointed out, you should call your inside components functions with this, and you need to bind these functions to this. There are multiple ways to do that, one of then is to bind it inside the component constructor like:
this.saveDashboard = this.saveDashboard.bind(this)
Other good thing to do is to return the axios call inside the saveDashboard() and getAllDashboards()
Related
I am building an application with pure javascript and Web Components. I also want to use the MVC Pattern, but now I have a problem with asynchronous calls from the model.
I am developing a meal-list component. The data is coming from an API as JSON in the following format:
[
{
id: 1,
name: "Burger",
},
]
I want the controller to get the data from the model and send it to the view.
meals.js (Model)
export default {
get all() {
const url = 'http://localhost:8080/meals';
let speisekarte = [];
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
// This prints the result I want to use, but I can't return
console.log(data);
// This does not work
speisekarte = data;
// This also does not work
return data;
});
// is undefined.
return speisekarte;
},
}
This is how I tried to get the data from an API.
meal-list.component.js (Controller)
import Template from './meal-list.template.js'
import Meal from '../../../../data/meal.js'
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
// Should send the Data from the model to the View
this.shadowRoot.innerHTML = Template.render(Meal.all);
}
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-list.template.js (View)
export default {
render(meals) {
return `${this.html(meals)}`;
},
html(meals) {
let content = `<h1>Speisekarte</h1>
<div class="container">`;
content += /* display the data from api with meals.forEach */
return content + '</div>';
},
}
As I mentioned in the comments, I have a problem in returning the async data from the model to the view. Either it is undefined when I try to return data; or if I try to save the data into an array. I could also return the whole fetch() method, but this returns a promise and I dont think the controller should handle the promise.
I already read the long thread in How do I return the response from an asynchronous call? but I could not relate it to my case.
Since you declared speisekarte as an array, I'd expect it to always return as an empty array. When the fetch executes and fulfills the promise, its always too late in the above implementation.
You have to wait for the fetch result and there are multiple options you might consider:
Either providing a callback to the fetch result
Or notifying your application via event dispatch and listeners that your data has been loaded, so it can start rendering
Your link already has a very good answer on the topic callbacks and async/await, I could not put it better than what is explained there.
Thanks to lotype and Danny '365CSI' Engelman I've found the perfect solution for my projct. I solved it with custom events and an EventBus:
meal.js (model)
get meals() {
const url = 'http://localhost:8080/meals';
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
let ce = new CustomEvent(this.ESSEN_CHANGE_EVENT, {
detail: {
action: this.ESSEN_LOAD_ACTION,
meals: data,
}
});
EventBus.dispatchEvent(ce);
});
},
EventBus.js (from book: Web Components in Action)
export default {
/**
* add event listener
* #param type
* #param cb
* #returns {{type: *, callback: *}}
*/
addEventListener(type, cb) {
if (!this._listeners) {
this._listeners = [];
}
let listener = {type: type, callback: cb};
this._listeners.push(listener);
return listener;
},
/**
* trigger event
* #param ce
*/
dispatchEvent(ce) {
this._listeners.forEach(function (l) {
if (ce.type === l.type) {
l.callback.apply(this, [ce]);
}
});
}
}
Now, when the data is ready, a signal to the event bus is sent. The meal-list-component is waiting for the events and then gets the data:
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
this.dom = Template.mapDOM(this.shadowRoot);
// Load Speisekarte on init
this.dom.meals.innerHTML = Template.renderMeals(MealData.all);
// Custom Eventlistener - always triggers when essen gets added, deleted, updated etc.
EventBus.addEventListener(EssenData.ESSEN_CHANGE_EVENT, e => {
this.onMealChange(e);
});
}
onMealChange(e) {
switch (e.detail.action) {
case EssenData.ESSEN_LOAD_ACTION:
this.dom.meals.innerHTML = Template.renderMEals(e.detail.meals);
break;
}
}
}
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.
I am using Angular resourse to get my data from an API, in this way:
var getAccountListPerUser = function () {
return $resource(uri, {}, {
get: {
headers: service.getDefaultHeaderRequest(),
method: 'GET',
transformResponse: function (data) {
var accountList = [];
try {
accountList = JSON.parse(data);
} catch (e) {
accountList = [];
}
return accountList;
},
isArray: true,
cache: true
}
}).get().$promise;
};
In my controller I have to use it and another two service functions defined in the same way.
var promiseResourcesAccountList = usrWebUserService.getAccountListPerUser();
promiseResourcesAccountList.then(function(result){
$scope.usersWithAccountsAndProfiles = result;
var filteredProfiles = [];
for (var account in result) {
...
}
$scope.filteredProfiles = filteredProfiles;
});
And:
var promiseResourcesEditUser = usrWebUserService.getResourcesUser(currentUser);
promiseResourcesEditUser.then(function (result) {
usrWebUserFactory.mapBasicPreferences($scope, result);
});
And then another very similar, this information loads data in three divs, but I want to show them only when all the three functions have completed correctly. I think I have to chain the result of the promises. How can I do that?
You can chain them like:
promiseResourcesAccountList.then(function(result){
///whatever processing
//return a promise
return promiseResourcesEditUser()
}).then(function(){
return anotherPromise();
}).then(function(){
//update scope here
});
alternatively, you could also use $q.all([promise1, promise2, promise3]).then(...);
#terpinmd is correct. Chaining promises is pretty simple. Say you have a service with a "getWidgets" that returns a promise, and you want to use the response from that service to call another service, "getWidgetOwners" that will return another promise :
Assumptions
getWidgets returns an array of widget objects.
getWidgetOwners accepts an array of ownerIds
How To:
service.getWidgets()
.then(function(widgets) {
return widgets.map(function(widget) { // extract ownerIds
return widget.ownerId;
});
})
.then(service.getWidgetOwners) // pass array of ownerId's to
.then(function(owners) { // the next service
console.log(owners);
});
With the following code snipped in angular2. the url is working beginning given as aspected. If i run processTab without running it through chrome.tabs.query's asynchronous callback. it works perfectly but if i run it within the callback. the value is being passed to the processTab function but it is not working properly.
Not Working**
randomFunction() {
var self = this,
curl:string;
chrome.tabs.query({currentWindow: true, active: true}, function(tabs){
// self.updateUrl = tab.url.replace(/.*?:\/\//g, "")
curl = tabs[0].url.replace(/.*?:\/\//g, "").replace(/\/$/, "");
self.processTab(curl);
});
}
processTab(url:string) {
this.listService.getData(url)
.subscribe(
data => this.data = data,
error => this.errorMessage = <any>error);
console.log("the url: " + url);
}
Working:
randomFunction() {
this.processTab("www.whateverurl.com");
}
processTab(url:string) {
this.listService.getData(url)
.subscribe(
data => this.data = data,
error => this.errorMessage = <any>error);
console.log("the url: " + url);
}
but the value is being passed to processTab in both instances.
I assume chrome.tabs.query is not covered by zone.js. You need to make the code run explicitly inside Angulars zone for change detection detect model changes
class SomeClass {
constructor(private zone:NgZone) {}
randomFunction() {
curl:string;
chrome.tabs.query({currentWindow: true, active: true}, (tabs) => {
this.zone.run(() => {
// this.updateUrl = tab.url.replace(/.*?:\/\//g, "")
curl = tabs[0].url.replace(/.*?:\/\//g, "").replace(/\/$/, "");
this.processTab(curl);
});
});
...
}
}
also prefer arrow functions instead of self
I think I'm writing my promise incorrectly and I couldn't figure out why it is caching data. What happens is that let's say I'm logged in as scott. When application starts, it will connect to an endpoint to grab listing of device names and device mapping. It works fine at this moment.
When I logout and I don't refresh the browser and I log in as a different user, the device names that scott retrieved on the same browser tab, it is seen by the newly logged in user. However, I can see from my Chrome's network tab that the endpoint got called and it received the correct listing of device names.
So I thought of adding destroyDeviceListing function in my factory hoping I'll be able to clear the values. This function gets called during logout. However, it didn't help. Below is my factory
app.factory('DeviceFactory', ['$q','User', 'DeviceAPI', function($q, User, DeviceAPI) {
var deferredLoad = $q.defer();
var isLoaded = deferredLoad.promise;
var _deviceCollection = { deviceIds : undefined };
isLoaded.then(function(data) {
_deviceCollection.deviceIds = data;
return _deviceCollection;
});
return {
destroyDeviceListing : function() {
_deviceCollection.deviceIds = undefined;
deferredLoad.resolve(_deviceCollection.deviceIds);
},
getDeviceIdListing : function() {
return isLoaded;
},
getDeviceIdMapping : function(deviceIdsEndpoint) {
var deferred = $q.defer();
var userData = User.getUserData();
// REST endpoint call using Restangular library
RestAPI.setBaseUrl(deviceIdsEndpoint);
RestAPI.setDefaultRequestParams( { userresourceid : userData.resourceId, tokenresourceid : userData.tokenResourceId, token: userData.bearerToken });
RestAPI.one('devices').customGET('', { 'token' : userData.bearerToken })
.then(function(res) {
_deviceCollection.deviceIds = _.chain(res)
.filter(function(data) {
return data.devPrefix != 'iphone'
})
.map(function(item) {
return {
devPrefix : item.devPrefix,
name : item.attributes[item.devPrefix + '.dyn.prop.name'].toUpperCase(),
}
})
.value();
deferredLoad.resolve(_deviceCollection.deviceIds);
var deviceIdMapping = _.chain(_deviceCollection.deviceIds)
.groupBy('deviceId')
.value();
deferred.resolve(deviceIdMapping);
});
return deferred.promise;
}
}
}])
and below is an extract from my controller, shortened and cleaned version
.controller('DeviceController', ['DeviceFactory'], function(DeviceFactory) {
var deviceIdMappingLoader = DeviceFactory.getDeviceIdMapping('http://10.5.1.7/v1');
deviceIdMappingLoader.then(function(res) {
$scope.deviceIdMapping = res;
var deviceIdListingLoader = DeviceFactory.getDeviceIdListing();
deviceIdListingLoader.then(function(data) {
$scope.deviceIDCollection = data;
})
})
})
Well, you've only got a single var deferredLoad per your whole application. As a promise does represent only one single asynchronous result, the deferred can also be resolved only once. You would need to create a new deferred for each request - although you shouldn't need to create a deferred at all, you can just use the promise that you already have.
If you don't want any caching, you should not have global deferredLoad, isLoaded and _deviceCollection variables in your module. Just do
app.factory('DeviceFactory', ['$q','User', 'DeviceAPI', function($q, User, DeviceAPI) {
function getDevices(deviceIdsEndpoint) {
var userData = User.getUserData();
// REST endpoint call using Restangular library
RestAPI.setBaseUrl(deviceIdsEndpoint);
RestAPI.setDefaultRequestParams( { userresourceid : userData.resourceId, tokenresourceid : userData.tokenResourceId, token: userData.bearerToken });
return RestAPI.one('devices').customGET('', { 'token' : userData.bearerToken })
.then(function(res) {
return _.chain(res)
.filter(function(data) {
return data.devPrefix != 'iphone'
})
.map(function(item) {
return {
devPrefix : item.devPrefix,
name : item.attributes[item.devPrefix + '.dyn.prop.name'].toUpperCase(),
};
})
.value();
});
}
return {
destroyDeviceListing : function() {
// no caching - nothing there to be destroyed
},
getDeviceIdListing : function(deviceIdsEndpoint) {
return getDevices(deviceIdsEndpoint)
.then(function(data) {
return { deviceIds: data };
});
},
getDeviceIdMapping : function(deviceIdsEndpoint) {
return this.getDeviceIdListing(deviceIdsEndpoint)
.then(function(deviceIds) {
return _.chain(deviceIds)
.groupBy('deviceId')
.value();
});
}
};
}])
Now, to add caching you'd just create a global promise variable and store the promise there once the request is created:
var deviceCollectionPromise = null;
…
return {
destroyDeviceListing : function() {
// if nothing is cached:
if (!deviceCollectionPromise) return;
// the collection that is stored (or still fetched!)
deviceCollectionPromise.then(function(collection) {
// …is invalidated. Notice that mutating the result of a promise
// is a bad idea in general, but might be necessary here:
collection.deviceIds = undefined;
});
// empty the cache:
deviceCollectionPromise = null;
},
getDeviceIdListing : function(deviceIdsEndpoint) {
if (!deviceCollectionPromise)
deviceCollectionPromise = getDevices(deviceIdsEndpoint)
.then(function(data) {
return { deviceIds: data };
});
return deviceCollectionPromise;
},
…
};