Transform data before rendering it to template in meteor - javascript

I want to return a single document with the fields joined together. That is, a result like as follows
{
_id: "someid",
name: "Odin",
profile: {
game: {
_id: "gameid",
name: "World of Warcraft"
}
}
}
I have a route controller which is fairly simple.
UserController = RouteController.extend({
waitOn: function () {
return Meteor.subscribe('users');
},
showAllUsers: function () {
this.render('userList', {
data: Meteor.users.find()
})
}
});
I've tried changing my data like so:
this.render('userList', {
data: Meteor.users.find().map(function (doc) {
doc.profile.game = Games.findOne();
return doc;
})
});
However, this does not have the intended effect of adding "game" to the user. (and yes, Games.findOne() has a result)
How can you transform the results of a cursor in meteor and iron:router?

Try defining your data as a function so it can be dynamically re-executed when needed.
UserController = RouteController.extend({
waitOn: function () {
return Meteor.subscribe('users');
},
showAllUsers: function () {
this.render('userList', {
data: function(){
return Meteor.users.find().map(function (doc) {
doc.profile.game = Games.findOne();
return doc;
});
}
});
}
});

Given your use of easy search, what might be simpler is just to define a template helper for profile
Template.userList.helpers({
profile: function(){
var game = Games.findOne({_id: this.gameId});
return { game: { _id: game._id, name: game.name }};
}
});
This assumes a single game per user. If you have more than one then you can iterate over a cursor of Games instead.

Related

Global loaded data in VueJs is occasionally null

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.

Set object in data from a method in VUE.js

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])

How do I pass a dynamic page :id to $http.get url in Vue.js

I have have view router set up:
router.map({
'/tracks/:id': {
component: SingleTrack
}
})
And this is my component (which works with a hard coded URL):
var SingleTrack = Vue.component('track', {
template: '#track-template',
data: function() {
return {
track: ''
}
},
ready: function() {
this.$http.get('//api.trax.dev/tracks/1', function (data) {
this.$set('track', data.track)
})
}
});
How do I pass the url/:id to the end of the $http.get string so i can grab the correct data dynamically when that route in loaded, something like:
ready: function(id) {
this.$http.get('//api.trax.dev/tracks/' + id, function (data) {
this.$set('track', data.track)
})
}
You should be able to get the route parameter from the component $route property :
var itemId = this.$route.params.id;
this.$http.get('//api.trax.dev/tracks/' + itemId, function (data) {
this.$set('track', data.track)
})
See more details in vue.js router documentation
For Best Practises:
index.js(router)
{
path: '/tracks/:id',
name: 'SingleTrack',
component: SingleTrack,
props: (route) => {
const id = Number.parseInt(route.params.id);
return { id }
},
}
SingleTrack.vue
props: {
id: {
type: Number,
required: true,
},
},
mounted(){
this.$http.get('//api.trax.dev/tracks/' +this.id, function (data) {
this.$set('track', data.track)
})
}

Meteor: Reactive joins exposes intermediate data

I'm using publish-composite to perform a reactive join (I'm sure the specific package does not matter). And I am seeing that the intermediate data gets pushed to the client.
In the following example:
Meteor.publishComposite('messages', function(userId) {
return {
find: function() {
return Meteor.users.find(
{ 'profile.connections.$': userId }
);
},
children: [{
find: function(user) {
return Messages.find({author: user._id});
}
}]
}
});
All the users that has userId in profile.connections get exposed to the client. I know that can create a mongodb projection so the sensitive stuff is not exposed. But I was wondering if I can just prevent the first find() query cursor from getting to the client at all.
Are you trying to only publish messages for a particular user if that user is a connection with the logged on user? If so, maybe something like this would work:
Meteor.publishComposite('messages', function(userId) {
return {
find: function() {
return Meteor.users.find(this.userId);
},
children: [{
find: function(user) {
return Meteor.users.find(
{ 'profile.connections.$': userid }
);
},
children: [{
find: function(connection, user) {
return Messages.find({author: connection._id});
}
}]
}]
};
});
That would be equivalent to something like :
Meteor.publish('message',function(userId) {
var user = Meteor.users.find({_id : this.userId, 'profile.connections.$' : userId});
if (!!user) {
return Messages.find({author: userId});
}
this.ready();
});

Define autoform hook for forms in for each in Meteor

I have a for each loop in Meteor and I'm using autoform to update each item (just like described at http://autoform.meteor.com/update-each).
My problem is that I normally create notifications through hooks with
AutoForm.hooks({
myFormId: {
onSuccess: function(formType, result) {
Notifications.success('Title', 'Text.');
}
}
});
but since all my forms have unique IDs, I cannot use this. How can I create a hook which matches all forms in a template or has a name which matches a regular expression "unique-id-?" where ? is the docId?
This may not be the optimal solution, but it works:
Template["updateEach"].helpers({
items: function () {
return Items.find({}, {sort: {name: 1}});
},
makeUniqueID: function () {
return "update-each-" + this._id;
}
});
Template.updateEach.onRendered(function () {
var hooksObject = {
onSuccess: function (formType, result) {
Notifications.success('Title', 'Text.');
}
};
var formIds = Items.find().map(function (item) {
return "update-each-" + item._id;
});
AutoForm.addHooks(formIds, hooksObject);
});

Categories