I have a problem implementing a non shuffling each over a reactive collection.
With this I mean that I have a collection ordered by a score value, and I don't want to change the order even if a score change.
I'd like it to be reactive but without the list items changing position every time.
The current code is this
ListController = FastRender.RouteController.extend({
find: function (tag){
if(!!tag){
return Lib.Items.Tags.SupportedType[tag].find;
}
return {};
},
findOptions: function(order, pageNum) {
var findOption = { limit: Lib.Items.Settings.ItemsPerPage * (parseInt(pageNum) || this.increment)};
if(!!order){
_.extend(findOption, Lib.Items.ListOrders.SupportedType[order].findOption);
}
return findOption;
},
waitOn: function() {
return [
Meteor.subscribe('items.all', this.params.tag, this.params.order, this.pageNum),
];
},
data: function () {
return {
"items": Items.find(this.find(), this.findOptions(this.params.order, this.params.pageNum))
};
},
action: function(){
this.render();
}
});
[...]
this.route('list', {
path: '/list/:pageNum?',
controller: ListController
});
this.route('list_order_limit', {
path: '/list/order/:order/:pageNum?',
controller: ListController
});
this.route('list_tag_limit', {
path: '/list/tag/:tag/:pageNum?',
controller: ListController
});
this.route('list_tag_order_limit', {
path: '/list/tag/:tag/order/:order/:pageNum?',
controller: ListController
});
and
{{#each items}}
{{> item}}
{{/each}}
With this everything works but the list change orders (which is correct but not the behaviour I'd like).
I tried some different solutions but nothing worked.
I started by modifying the data.
I put
data: function () {
return {
"items": Items.find(this.find(), this.findOptions(this.params.pageNum))
};
},
In this way the data knows nothing about the ordering (only the subscription knows about which order is used to take the first number of items items).
The problem is that in this way even if the route change from one ordering to another the list is not updated. And while this happens I see the subscription getting called with the new order parameter (so the data sent via ddp should actually be correct).
I even tried stopping and resubscribing but nothing changed.
What I'm doing wrong? Is there a better way to have a list reactive in its item but not in its order?
Thanks in advance.
I don't see anything in your code that explicitly references the order by the score value; however I believe this is an unresolved issue in Meteor: https://github.com/meteor/meteor/issues/1276.
One of the workarounds suggested in that thread was to add an explicit sort by _id on the client, which will ensure that the cursor is stable even when items change.
I can imagine a more unconventional way to work around this, which is to compute the desired order in the publish function and use the publish API to send custom fields over to the client, and then sorting by these fields. This is more complicated than a cursor but will result in the items showing up in the exact order determined at publish time.
Related
I'm building an angular component that renders a table, and I'm running into some issues with the sorting function. The scope in this case looks like this:
$scope.listState = {
sortBy: '<string>',
sortReverse: <bool>,
headings: {...},
list: [
{
rowCols: {
user: 'timfranks#gmail.com',
name: 'Tim Franks',
since: '11/6/12'
}
rowState: {...}
},
{
{
user: 'albertjohns#sbcglobal.net',
name: 'Alber Johns',
since: '11/12/13'
},
rowState: {...}
},
{
{
user: 'johnsmith#sine.com',
name: 'John Smith',
since: '7/28/14'
},
rowState: {...}
}
]
};
I originally tried to sort the list via:
ng-repeat="row in $ctrl.list | orderBy:$ctrl.listState.sortBy:$ctrl.listState.sortReverse"
This didn't work, although in tracing with the debugger I found that orderBy was in fact getting the right arguments and returning a properly sorted list. Just to be sure, I changed the code to use orderBy in my controller, like this:
this.listState.list = _this.orderBy(listState.list, 'rowCols.' + listState.sortBy, listState.sortReverse);
This works for the first time (called within the constructor), but then stops working. I'm pretty sure this is some aspect of Angular's scope that I don't fully understand. Any help would be appreciated.
Using a filter in an ng-repeat expression does not update the original model, but rather creates a copy of the array.
You are on the right track with the this.listState.list = _this.orderBy(...) ... and it does make sense that it gets called only once.
If the listState.list model is getting new values after the controller loads and you want to resort with those new values, you would probably want to use something like:
$scope.$watchCollection('listState.list', function listChanged(newList, oldList){
$scope.listState.list = _this.orderBy(...);
});
I can't recall if $watchCollection is going to register a change if the order changes, but if you end up in an infinite loop with that code, you could put a blocker like:
var listSorting = false;
$scope.$watchCollection('listState.list', function listChanged(newList, oldList){
if(!listSorting){
listSorting = true;
$scope.listState.list = _this.orderBy(...);
$timeout(function resetSortBlock(){ // try it without the $timeout to see if it will work
listSorting = false;
});
}
});
Figured it out. The issue was that I used an attribute directive to render the list rows, and the attribute directive and ng-repeat were on the same element. This created a scope conflict between the two directives. I moved my directive to a div within the ng-repeat and it works fine.
We have a layout that needs the current data that is defined in our controller like this:
TripController = RouteController.extend({
layoutTemplate: 'tripLayout',
data: function () {
return Trips.findOne({_id: this.params._id});
}
});
Our problem seems like a data-race:
Most of the times Template.currentData() is null but sometimes (mostly while debugging) it is our data that we defined.
The problem only occurs when we reload the page while we are in the tripLayout, but not when we enter the trip layout from another page.
Note that we thing that the `TripController is loaded, as the controller params is correctly set.
Template.tripLayout.onRendered(function() { // note, this is the layout template
Iron.controller.params._id // correct id
Template.currentData() // null
});
What we are trying to achieve is a split layout where the right side is always the same and the left side is filled by yield (see screenshot)
UPDATE
(THIS UPDATE IS WRONG)
I think I found the error:
waitOn: function () {
return [Meteor.subscribe('public-trips'),
Meteor.subscribe('my-trips')];
}
As soon as I remove the array (so only one subscription), the
data: function () {
return Trips.find({}).fetch();
}
does not return 0 anymore. I will look into it a bit more.
UPDATE2
So my initial assumption was wrong, subscribing to only one does not help. It is really just a race condition.
UPDATE3
I managed it to reproduce it: meteorpad
In the alerts, it shows the number of players it has. First time it is 0 and then 6. But the 0 should not appear, as we 'waitOn' it?!
We managed to solve the 'problem' (It is expected behaviour, but we found a workaround)! Details at https://github.com/iron-meteor/iron-router/issues/1464#issuecomment-158738943:
Alright, here's how we fixed that problem:
The first important thing is to to include the action function like
so:
action: function() {
if (this.ready()) {
this.render();
}
}
That worked for almost everything, but we still had some issues. In
our case, it was about templates that aren't rendered in using yield,
and are thus out of Iron Router's control. Switch these over to
content regions (using {{yield 'contentRegionName'}}) and then
explicitly render them in the action function like so:
action: function() {
if (this.ready()) {
this.render();
this.render('contentRegionTemplate', {to: 'contentRegionName'});
}
}
You need these action functions, since the waitOn only means that the
subs defined there will be added to a waitlist, but not that that
waitlist will be waited on. Essentially, what happens now is that your
action function is called twice, thanks to reactivity, and the
templates are only rendered once the data is ready.
I have a very big JSON in my controller fetched from a MongoDB database. Here's its structure (haven't added irrelevant keys):
$scope.keyword; //ignore for now explained below
$scope.data = [
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value} //so on
]
},
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value}
]
},
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value}
]
}
]
Here's my HTML template displaying the JSON:
<div ng-repeat="outer in data">
<div ng-repeat="inner in outer.key | keywordFilter: keyword">
//print inner values
</div>
</div>
Here, 'keyword' holds values which will be used to filter the inner ng-repeat loop. Here's my filter:
app.filter('keywordFilter', function() {
return function(collection, keyword) {
//Iteration over the entire collection. If keyword exists add the item to output
return output;
}
})
Now, as expected, the filter runs whenever I modify '$scope.keyword'. It is updated when I click on a button as follows:
HTML:
<input type="text" ng-model="temp" />
<button ng-click="updateKeyword()">
Controller:
$scope.updateKeyword = function() {
$scope.keyword = $scope.temp;
}
The Problem:
Due to the sheer size of data, the filtering process is taking around 10-15 seconds (sometimes the browser hangs!) and THEN the new filtered data returned from the filter is displayed on the screen.
The Requirement:
What I want to do is that during these 10-15 seconds I want to show a loader and after the calculations are done, the news is displayed. How do I achieve this?
What I tried:
I figured that once I click on the filter button, I would need to wait for the ng-repeat loop to finish and thus I tried triggering an event on the finish of ng-repeat by referring this thread.
But what I found out is that the event is triggered ONLY when the data is displayed for the first time and not when the filter button is clicked and the data is filtered by the keyword filter.
You could maybe simply invalidate you action when angular is in digest:
in your controller:
function isInDigest(){
$scope.$$phase ? true : false;
}
$scope.updateKeyword = function() {
if(isInDigest()){ return; }
$scope.keyword = $scope.temp;
}
and in your template
<button ng-class="{'inactive': isInDigest()}" ng-click="updateKeyword()">
Just use angular's $timeout, which runs the function it receives as parameter after angular has manipulated and the browser has rendered the DOM.
For more info, I recommend this and this stackoverflow answer discussing some timings in connection with $timeout and $evalAsync, with some very authoritative references.
But I concur with estus' comment: this might not be the architecture you're looking for, use javascript in the controller or a directive to leave less work for ng-repeat, or use lazy loading mechanisms if your data is so large.
I'm trying to implement a basic route using Flow Router. But no matter what _id of a collection document I request; I always only get the info about the first document in my collection - 'Requests'.
So here's my route definition in the file /lib/routes.js:
FlowRouter.route('/requests/:reqId', {
subscriptions: function (params, queryParams) {
this.register('theRequest', Meteor.subscribe('singleRequest', params.reqId));
},
action: function (params, queryParams) {
FlowLayout.render('layout', { aside: 'aside', main: 'singleRequest' });
console.log("Yeah! We are on the post:", params.reqId);
},
name: 'aRequest'
});
Here's my helper:
Template.singleRequest.helpers({
getRequest: function () {
return Requests.findOne();
}
});
Here's my server publish:
Meteor.publish('singleRequest', function (reqId) {
return Requests.find({ _id: reqId});
});
And here's the template:
<template name="singleRequest">
{{#if isSubReady 'theRequest'}}
{{#with getRequest}}
<h2>{{_id}}</h2>
<p>{{reqFrom}}</p>
<p>{{reqBy}}</p>
{{/with}}
{{else}}
loading...
{{/if}}
</template>
What am I doing wrong?
Note: In the console, I do see right 'reqId' slug due to the console.log command in the route definition. But I do not see the relevant info for the document which it belongs to.
Thanks!
OK my problem was that I previously had another subscription where I published all the Requests - not just the one with that certain _id. And because I did not create the helper to get only the one with that certain _id; of course server just sent me the very first request.
My solution was only to subscribe to previous subscription and define in the helper to fetch to the request _id:
FlowRouter.route('/requests/:reqId', {
subscriptions: function (params, queryParams) {
this.register('allRequests', Meteor.subscribe('Requests', params.reqId));
},
action: function (params, queryParams) {
FlowLayout.render('layout', { aside: 'aside', main: 'singleRequest' });
console.log("Yeah! We are on the post:", params.reqId);
},
name: 'aRequest'
});
and the helper:
Template.singleRequest.helpers({
getRequest: function () {
var reqId = FlowRouter.getParam("reqId")
return Requests.findOne({_id:reqId});
}
});
For anyone who browses to this question looking for how to get Flow Router to capture and dynamically link to slugs from the db, then call a template page for each item, I made a very simple example app and posted it on here on GitHub.
Hope it will help someone.
I believe your code is correct for what you are trying to do. However, I infer from your direct replication of the code from flow-router that you're pretty new to Meteor. Therefore, I'm willing to take a punt that you still have the autopublish package in your app.
Autopublish pushes all data in your collection to the client. Even without a publish/subscribe call.
You can do two things. To keep autopublish (which will make your development process easier at the start but maybe harder later on), just change your template helper to:
Template.singleRequest.helpers({
getRequest: function () {
return Requests.findOne({ _id: FlowRouter.getParam("reqId")});
}
});
Alternatively (and you will want to do this eventually), go the the command line and type:
meteor remove autopublish
You can read up on the pros and cons of autopublish in lots of places to make your own decision on which option is best for you. However, you should also consider whether or not you want to cache your data in future (e.g. using subsManager) so you may want to change your template helper regardless.
This is code from an Angular introduction video series, which explains how to populate angular controllers with data from persisted memory, but stops just short of explaining how to add the new product reviews to the persisted memory.
There do seem to be some articles explaining how to do this, but since I am very new to angular, I'm afraid I couldn't understand any of them.
I have figured out the syntax for making post requests using $http, but I don't see how to fit that code into the existing structure, so that it will 1) be called when pushing a new element to the reviews array, and 2) update the view when completed.
I am interested to learn a basic way to add the new product review to persistent memory.
(function() {
var app = angular.module('gemStore', ['store-directives']);
app.controller('StoreController', ['$http', function($http){
var store = this;
store.products = [];
$http.get('/store-products.json').success(function(data){
store.products = data;
});
}]);
app.controller('ReviewController', function() {
this.review = {};
this.addReview = function(product) {
product.reviews.push(this.review);
this.review = {};
};
});
})();
The JSON looks like this:
[
{
"name": "Azurite",
"description": "...",
...
"reviews": []
},
...
]
If the store-products.json is just a file on the server, you'll need an actual backend implementation (in PHP, nodejs, etc.) to actually update the file (or more typically just return the content from the database).
Normally you would make a save method and not post on every modification, though. But, in either case, depending on your backend, usually the implementation is as simple as $http.put('/store-products', store.products) whenever you click a "save" button. Typically, the put can return the same data, so there's typically no need to update the view since you just set it exactly to your state. But, if you have possibility of concurrent editing, and the put returns the modified data, it would look like your get:
$http.put('/store-products', store.products).success(function(data){
store.products = data;
});
For adding an item, it might almost identical, depending on your data model:
$http.post('/store-products', newProduct).success(function(data){
store.products = data;
});
In this case the POST gives an item to add and returns all of the products. If there are a lot of products -- that is, products are more like a large database than a small set in a "document", then the post would more typically return the added item after any server processing:
$http.post('/store-products', newProduct).success(function(newProductFromServer){
store.products.push(newProductFromServer); //if newProduct wasn't already in the array
//or, store.products[newProductIdx] = newProductFromServer
});
If you really wanted to call this function on every modification instead of a save button, you can use a watch:
$scope.$watchCollection(
function() { return store.products; },
function() { /* call the $http.put or post here */ }
}