Vue.js - Global Data from AJAX Call - javascript

I'm giving Vue.js a try and so far I'm loving it because it's much simpler than angular. I'm currently using vue-router and vue-resource in my single page app, which connects to an API on the back end. I think I've got things mostly working with a the primary app.js, which loads vue-router and vue-resource, and several separate components for each route.
Here's my question: How do I use props to pass global data to the child components when the data is fetched using an asynchronous AJAX call? For example, the list of users can be used in just about any child component, so I would like the primary app.js to fetch the list of users and then allow each child component to have access to that list of users. The reason I would like to have the app.js fetch the list of users is so I only have to make one AJAX call for the entire app. Is there something else I should be considering?
When I use the props in the child components right now, I only get the empty array that the users variable was initialized as, not the data that gets fetched after the AJAX call. Here is some sample code:
Simplified App.js
var Vue = require('vue');
var VueRouter = require('vue-router')
Vue.use(VueRouter);
var router = new VueRouter({
// Options
});
router.map({
'*': {
component: {
template: '<p>Not found!</p>'
}
},
'/' : require('./components/dashboard.js'),
});
Vue.use(require('vue-resource'));
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
users: [],
};
},
methods: {
fetchUsers: function() {
this.$http.get('/api/v1/users/list', function(data, status, response) {
this.users = data;
}).error(function (data, status, request) {
// handle error
});
}
}
});
router.start(App, '#app')
Simplified app.html
<div id="app" v-cloak>
<router-view users = "{{ users }}">
</router-view>
</div>
Simplified dashboard.js
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
},
};
When dashboard.js gets run, it prints an empty array to the console because that's what app.js initializes the users variable as. How can I allow dashboard.js to have access to the users variable from app.js? Thanks in advance for your help!
p.s. I don't want to use the inherit: true option because I don't want ALL the app.js variables to be made available in the child components.

I believe this is actually working and you are being misled by the asynchronous behavior of $http. Because your $http call does not complete immediately, your console.log is executing before the $http call is complete.
Try putting a watch on the component against users and put a console.log in that handler.
Like this:
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
watch: {
users: {
handler: function (newValue, oldValue) {
console.log("users is now", this.users);
},
deep: true
}
}
}
};

In the new version of Vue 1.0.0+ you can simply do the following, users inside your component is automatically updated:
<div id="app" v-cloak>
<router-view :users="users"></router-view>
</div>

Related

Wait for data in mapstate to finish loading

I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly.
Is there a way to wait for the data to be finished loading?
Here is how userProfile is set in Vuex:
mutations: {
setUserProfile(state, val){
state.userProfile = val
}
},
actions: {
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.teachersCollection.doc(user.uid).get()
// set user profile in state
commit('setUserProfile', userProfile.data())
},
}
Here is the code where I want to acess it:
<template>
<div>
<h1>Test</h1>
{{userProfile.firstname}}
{{institute}}
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
data() {
return {
institute: "",
}
},
computed: {
...mapState(['userProfile']),
},
created(){
this.getInstitute();
},
methods: {
async getInstitute() {
console.log(this.userProfile); //is here still empty at initial page load
const institueDoc = await this.userProfile.institute.get();
if (institueDoc.exists) {
this.institute = institueDoc.name;
} else {
console.log('dosnt exists')
}
}
}
}
</script>
Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation.
I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.
Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.
If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if
<template v-if="userProfile.id">
<!-- your normal html here... -->
</template>
<template v-else>
loading user profile...
</template>
To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:
export default {
data: () => ({
institute: ''
}),
computed: {
...mapState(['userProfile']),
},
watch: {
'userProfile.institute': {
async handler(institute) {
if (institute) {
const { name } = await institute.get();
if (name) {
this.institute = name;
}
}
},
immediate: true
}
}
}
Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.

How to reset all components in Vue.js

I'm trying to restart my component when the route changes:
beforeRouteUpdate (to, from, next) {
Object.assign(this.$data, this.$options.data())
console.log(to.query);
next();
}
but it doesn't work; it only prints the console.log. I've also tried this.$options.data.call(this) and apply.
In order to force Vue to re-render same component upon route query change, it is possible to assign a key to the <router-view it mount's into and push router to the page with the same route name or path.
Example:
Mounting point:
<router-view
:key="$route.fullPath"
/>
Component navigation, assuming route name is blog
<router-link :to={ name: 'blog', query: { count: 10 } }>Link to the same route</router-link>
This is assuming that you want to reset data of page component while navigating to the same page component with different data.
If component that you want to reset is not the route component, it is possible to reset it's data with watch option, while saving original data.
Example:
data () {
return {
initialData: {
// some initial data
},
data: {}
}
},
watch: {
'$route.fullPath': {
immediate: true, // Immediate option to call watch handler on first mount
handler () {
this.resetData()
}
}
},
methods: {
resetData () {
this.data = Object.assign({}, this.initialData)
},
},
Note, that any $route options can be watched and additional conditions added to handler via next and previous arguments.
Try
this.$router.reload()

ember set component data from application controller

I am new to Ember and trying to figure out how the data routing works. I have a 'page-notices' component and template thats included in my application.hbs file. It handles showing error or other notifications to users. I cannot figure out how to set the data inside the component from the application controller.
When a user triggers a logout action in application controller, I send it to the server via ajax json request and then need to update the page-notices component if there was an error returned. What does the page-notices controller need to look like to get this done? Am I asking the wrong question and shouldn't be using the controller for this?
//app/templates/application.hbs
{{app-header}}
<div id="pagecontent">
{{page-notices}}
<div id="wrapper">
{{outlet}}
<div class="push"></div>
</div>
</div>
{{app-footer}}
//app/controllers/application.js
import Ember from 'ember';
import ENV from '/config/environment';
var $ = Ember.$;
export default Ember.Controller.extend({
session: Ember.inject.service('session'),
pagenotices: Ember.inject.controller("page-notices")
actions: {
logout: function() {
var self = this;
$.ajax({
dataType: "json",
method: 'GET',
url: ENV.APP.apiHost,
data: {p: 'logout'},
success: function( response ){
if( response.success || (response.loggedin == false) ){
self.get('session').invalidate();
self.transitionToLoginRoute();
} else {
self.get('pagenotices').set('pageerrors', response.error);
self.get('pagenotices').set('pageerrorsview', '');
}
}
});
},
},
transitionToLoginRoute: function() {
this.transitionToRoute('login');
},
});
//app/templates/components/page-notices.js
<div id="pagenotices" class="{{pagenoticeview}}">
<div id="pageerrors" class="error centered {{pageerrorsview}}">{{pageerrors}}</div>
<div id="pagemessages" class="notice centered {{pagemessagesview}}">{{pagemessages}}</div>
</div>
//app/components/page-notices.js
import Ember from 'ember';
import ENV from '/config/environment';
const { inject: { service }, Component } = Ember;
export default Component.extend({
pagenoticeview: 'hide',
pageerrors: '',
pageerrorsview: 'hide',
pagemessages: '',
pagemessagesview: 'hide',
});
I did not quite understand why you injected page-notices controller to application controller; because you have already put page-notices component directly to application.hbs. I might be wrong but I got the sense that you are confusing a controller and a component.
Anyway,the following should work.
Remove pagenotices: Ember.inject.controller("page-notices") this; since we have no work with pagenotices controller as I explained above.
Change the else part in ajax handler of logout action within application.js as follows:
self.set('pageerrors', response.error);
self.set('pageerrorsview', '');
So that the corresponding attributes are directly saved to application controller itself.
Pass the corresponding attributes to page-notices component from within application.hbs with
{{page-notices pageerrors=pageerrors pageerrorsview=pageerrorsview}}
Declare the initial values of pageerrors and pageerorsview within application.js and remove them from page-notices component if you want. I mean declarations of pageerrors: '', pageerrorsview: 'hide'
This should work if I got what you are asking right, best regards.

Emberjs How to update property on a component from route?

Hi I would like to know what's the proper way to update a property on a component from the route?.
A little background on what I want to do:
I have two custom Buttons that I called CardButtons (based on material desing) next to one blank area called description, what I want is to create a hover event that triggers an ajax call to retrive detailed data from a data base and render it on the description area.
CHECK UPDATE
So far I have created a route like this:
export default Ember.Route.extend({
selectedModule: '',
model: function () {
return {
selectedModule: 'employeeModule'
};
},
actions: {
showDescription: function (params) {
this.set('model.selectedModule', params);
}
}});
My route template call my component like this:
<div class="row">
{{sis-db-description-render idTitle=model.selectedModule}}
</div>
and the component is defined like this:
export default Ember.Component.extend({
info: null,
ready: false,
didInsertElement: function () {
this.queryData();
},
queryData: function (){
/** Does an Ember.$.post request to the API with the idTitle as param**/
}
});
the first time this executes it load perfectly the detail data but when I try to refresh the data the event does not trigger a second call. I bealive it is beacause I'm not updating the model in a proper way.
Any idea on how to update the component property?
UPDATE:
Thanks to #kumkanillam I was able to find a way on my route I added the next code:
setupController: function (controller, model) {
this._super(...arguments); //if its new/index.js route file then you can use controller argument this method.
controller.set('selectedModule', 'employeeModule');
},
actions: {
showDescription: function (params) {
console.info(params);
this.controllerFor('new.index').set('selectedModule', params);
}
}
By doing so now the view updates the content every time, I still don't know if this is the correct way to do it but it works for now.
In the below code, model is not defined in route. it's defined in corresponding controller through setupController hook.
showDescription: function (params) {
this.set('model.selectedModule', params);
}
So in your case either you can define action in controller and update model.selectedModule
If you want to do it in route,
showDescription: function (params) {
let cont = this.controllerFor('route-name');
cont.set('model.selectedModule', params);
}

How to asynchronously load and append data to model on checkbox change of component?

I am currently developing an ember application which has two components.
One component represents a map the other one represents a friendslist.
Both components are placed in the same handlebar template.
What I try to achieve is that a user can check a checkbox in the friendslist component and in the next step his or her posts are loaded asynchronously from facebook (the friend itself was already loaded in the beforeModel hook). Those asynchronously loaded posts should be append to the already existing friend object in the model. Afterwards the map component should be informed about the changes and refresh itself or call a function which will draw points on the map.
At the moment I am trying to set the checked property of a single friend (which would be the same approach as appending the posts but will be easier for now):
// index.hbs
{{map-widget posts=model.posts friends=model.friends}}
{{friends-list checkedFriend='checkedFriend' friends=model.friends}}
// friends-list.hbs (component)
<ul>
{{#each friends as |friend|}}
<li>
{{input type="checkbox" id=friend.facebookID checked=friend.checked change=(action checkedFriend)}} <p>{{friend.name}}</p>
</li>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function () {
this.sendAction('checkedFriend');
}
}
});
// index.js (route)
export default Ember.Route.extend(AuthenticatedRouteMixin, {
...
model: function() {
return Ember.RSVP.hash({
posts: this.get('currentUserPosts'),
friends: this.get('friends')
});
},
actions: {
checkedFriend: function () {
// Update just the first friend here to see if the approach works
// Get the friends array from the model
const model = this.controller.get('model');
const friends = model.friends;
// Update the friend
Ember.set(friends[0], 'checked', true);
// Map component receives an update here,
// but "DEPRECATION: You modified (mut model.friends) twice in a single render." warning occurs
this.set('friends', friends);
}
}
})
The current approach works more or less. However, I get a depreciation warning that I modified the model twice in a single render which in my opinion is a sign for a bad design from myside.
What I would like know is how a good approach for my task described above would look like. If I am already on the right way I would be glad if anyone could tell me why this double rendering error appears.
The core problem is how to correctly update the model and how to inform the components especially the component which did not set the action about the changes so that those are refreshed.
Thank you in advance.
You could make a Class - FriendEntry. By calling its constructor you will recieve an instance of FriendEntry. Now you will be modifying instance instead of original record (which indeed is not right).
var FriendEntry = Ember.Object.extend({
init: function() {
this._super(...arguments);
this.set('somethingFriendly', true);
}
});
export default Ember.Controller.extend({
friendsEntries: Ember.computed.map('model.friends', function(friend) {
// Call the constructor
return FriendEntry.create({
friend: friend,
checked: false,
posts: []
})
})
});
Ok so your component would look something like this.
{{friends-list checkedFriend='changeFriendCheckedStatus' entries=friendEntries}}
// friends-list.hbs (component)
<ul>
{{#each entries as |entry|}}
{{input type="checkbox" checked=entry.friend.checked change=(action checkedFriend entry)}} <p>{{entry.friend.name}}</p>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function (entry) {
this.sendAction('checkedFriend', entry);
}
}
});
Back to controller
actions: {
changeFriendCheckedStatus(friendEntry) {
ic.ajax.request(API.HOST + '/someUrlForPosts/' + friendEntry.get('id)).then(givenFriendPosts => {
entry.get('posts').pushObjects(givenFriendPosts);
})
}
}
If i understood correctly you have 2 models I friends and posts (DS.belongsTo('friend')). You would need to encapsulate both into friendEntry (friend, posts).
So your map-widget would also look like this {{map-widget friendEntries=friendEntries}}
Instead of querying posts in model you could encapsulate them like this.
friendsEntries: function() {
return DS.PromiseArray.create({
promise: Ember.RSVP.all(this.get('model.friends')).then(friends => {
return friends.map(friend => {
return FriendEntry.create({
friend: friend,
checked: false,
posts: store.query('posts', { friend: friend.get('id') }
});
});
})
});
}.property('model.friends.[]')

Categories

Resources