I am having issues with pagination I've implemented. Pagination works based on server-sided publish skip & limit filter.
Issue #1.
If I perform a specific user search the very first page will be blank.
At that state skip is set to 0, limit is always 20.
If I perform a find().fetch() I get 20 elements, but they are all for a different user.
Issue #2 Me going to next page (skip+10) gives me a few more elements
Doing it again gives even more results
and finally being out of data, and going to next page just removes 10 results, leaving 10 shown
This is a very odd behavior.
Server-sided publish
Meteor.publish('overtime', function(opt){
if (!Roles.userIsInRole(this.userId, 'admin')) {
return Overtime.find({userId: this.userId}, opt);
} else {
return Overtime.find({}, opt);
}
});
Client-sided subscription
var defaultSkipStep = 10;
var defaultLimit = 20;
Template.triphtml.onCreated(function(){
var instance = this;
Session.set('limit',defaultLimit);
instance.autorun(function(){
instance.subscribe('overtime', {skip:(Session.get('overSkip')||0), limit:(Session.get('limit')||defaultLimit), sort: {createdAt: -1}});
instance.subscribe('trips', {skip:(Session.get('tripSkip')||0), limit:(Session.get('limit')||defaultLimit), sort: {createdAt: -1}});
});
Next page click event
"click .nxtpage_over": function(event, template){
Session.set('overSkip', (Session.get('overSkip') || 0) + defaultSkipStep);
Session.set('limit', 20);
},
Submit event
https://pastebin.com/btYCSQBD
Query that user sees
main.js (client)
https://pastebin.com/tWakPDT1
main.html
https://pastebin.com/4uMVFsNG
Any idea how to make it so that when I perform search for a specific user I get all 20 results just for that user, and next page gives me NEXT 20 elements, not showing any one the 20 I've just seen.
Use below code for pagination. It is very ideal code, easy to understand and implement.
SERVER PUBLISH:
Meteor.publish('Students', function (str, toSkip, tillLimit) {
var regex = new RegExp(str, 'i');
var data = Students.find( {
$or:[
{"_id": {'$regex' : regex}},
{"username": {'$regex' : regex}}
]
},
{sort: {'time': -1}, skip: toSkip, limit : tillLimit});
return data;
});
Meteor.publish('StudentsTotalCount', function (str) {
var regex = new RegExp(str, 'i');
Counts.publish(this, "StudentsTotalCount", Students.find({
$or:[
{"_id": {'$regex' : regex}},
{"username": {'$regex' : regex}}
]
});
);
});
str is global search text from client side. Now, Since the user will click on next and previous button frequently. On the client side the Subscription must be reactive. For that you can you below code.
CLIENT SUBSCRIBE:
Inside some App_ShowStudents.js file, you can create subscription as below;
Template.App_ShowStudents.onCreated(function(){
this.userId = new ReactiveVar("");
this.filterCriteria = new ReactiveVar("");
//PAGINATION
this.enablePrevious = new ReactiveVar(false);
this.enableNext = new ReactiveVar(true);
this.skip = new ReactiveVar(0);
this.diff = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.limit = new ReactiveVar(10);
this.autorun(() => {
Meteor.subscribe('Students', this.filterCriteria.get(), this.skip.get(), this.limit.get());
Meteor.subscribe('StudentsTotalCount', this.filterCriteria.get(), this.role.get());
});
});
HELPERS:
Template.App_ShowStudents.helpers({
students(){
return Students.findOne({}); // this will always be less or equal to 10.
},
skip(){
return Template.instance().skip.get();
},
diff(){
var instance = Template.instance();
var add = instance.skip.get() + instance.limit.get();
var total = instance.total.get();
var diff = (add >= total ? total : add);
instance.diff.set(diff);
return instance.diff.get();
},
total(){
Template.instance().total.set(Counts.get('StudentsTotalCount'));
return Template.instance().total.get();
}
});
total actually must be reactive and should give only count as per your search criteria, so we had a seperate publish on server.
EVENTS:
Template.App_ShowStudents.events({
'click .previous': function (event, template) {
event.preventDefault();
if(template.diff.get() > template.limit.get()){
template.skip.set(template.skip.get() - template.limit.get());
}
},
'click .next': function (event, template) {
event.preventDefault();
if(template.diff.get() < template.total.get()){
template.skip.set(template.skip.get() + template.limit.get());
}
},
'input #searchData': function( event, template ) {
if ( $( event.target ).val()) { // has some value.
template.filterCriteria.set( $( event.target ).val() );
} else {
template.filterCriteria.set( "" );
}
console.log(template.filterCriteria.get());
},
});
App_ShowStudents.html
<template name="App_ShowStudents">
{{#if Template.subscriptionsReady}}
<body>
<input type="text" id="searchData" class="form-control input-lg" placeholder="Search ID, Username" />
Showing {{skip}} - {{diff}} of {{total}}
{{#each students}}
...
{{/each}}
<ul>
<li>Previous</li>
<li>Next</li>
</ul>
</body>
{{else}}
Please wait...
{{/if}}
</template>
Related
I try to learn SAPUI5 with Samples frpm Demo kit Input - Checked. I get an error message: oInput.getBinding is not a function
I have a simple input field xml:
<Label text="Name" required="false" width="60%" visible="true"/>
<Input id="nameInput" type="Text" enabled="true" visible="true" valueHelpOnly="false" required="true" width="60%" valueStateText="Name must not be empty." maxLength="0" value="{previewModel>/name}" change= "onChange"/>
and my controller:
_validateInput: function(oInput) {
var oView = this.getView().byId("nameInput");
oView.setModel(this.getView().getModel("previewModel"));
var oBinding = oInput.getBinding("value");
var sValueState = "None";
var bValidationError = false;
try {
oBinding.getType().validateValue(oInput.getValue());
} catch (oException) {
sValueState = "Error";
bValidationError = true;
}
oInput.setValueState(sValueState);
return bValidationError;
},
/**
* Event handler for the continue button
*/
onContinue : function () {
// collect input controls
var that = this;
var oView = this.getView();
var aInputs =oView.byId("nameInput");
var bValidationError = false;
// check that inputs are not empty
// this does not happen during data binding as this is only triggered by changes
jQuery.each(aInputs, function (i, oInput) {
bValidationError = that._validateInput(oInput) || bValidationError;
});
// output result
if (!bValidationError) {
MessageToast.show("The input is validated. You could now continue to the next screen");
} else {
MessageBox.alert("A validation error has occured. Complete your input first");
}
},
// onChange update valueState of input
onChange: function(oEvent) {
var oInput = oEvent.getSource();
this._validateInput(oInput);
},
Can someone explain to me how I can set the Model?
Your model is fine and correctly binded.
The problem in your code is here, in the onContinue function
jQuery.each(aInputs, function (i, oInput) {
bValidationError = that._validateInput(oInput) || bValidationError;
});
aInput is not an array, so your code is not iterating on an array element.
To quickly fix this, you can put parentheses around the declaration like this:
var aInputs = [
oView.byId("nameInput")
];
Also, you could remove the first two lines of the _validateInput method since they are useless...
Usually, we set the model once the view is loaded, not when the value is changed. For example, if you would like to set a JSONModel with the name "previewModel", you can do as mentioned below.
Note that onInit is called when the controller is initialized. If you bind the model properly as follows, then the oEvent.getSource().getBinding("value") will return the expected value.
onInit: function(){
var oView = this.getView().byId("nameInput");
oView.setModel(new sap.ui.model.json.JSONModel({
name : "HELLO"
}), "previewModel");
},
onChange: function(oEvent) {
var oInput = oEvent.getSource();
this._validateInput(oInput);
},
...
Also, for validating the input text, you can do the following:
_validateInput: function(oInput) {
var oBinding = oInput.getBinding("value");
var sValueState = "None";
var sValueStateText = "";
var bValidationError = false;
if(oBinding.getValue().length === 0){
sValueState = "Error";
sValueStateText = "Custom Error"
}
oInput.setValueState(sValueState);
if(sValueState === "Error"){
oInput.setValueStateText(sValueStateText);
}
return bValidationError;
},
Please note that the code above is not high quality and production ready as it's a quick response to this post :)
I am using bootstrap typeahead (GitHub) to create search forms. After user inputs something I am doing elasticsearch query using elasticsearch.js and returning results. The problem is that results displayed in typeahead are always one character behind, not suggesting correct values.
Typeahead input:
<input type="text" data-provide="typeahead" class="form-control typeahead" id="searchInputId" placeholder="Search" autocomplete="off">
Here is my code:
var elasticsearchAddress = "exampleserver.com:9200";
var elasticsearchClient = createElasticsearchClient(elasticsearchAddress);
var data = [];
$("#searchInputId").typeahead({ source:data, items:10, fitToElement:true });
$("#searchInputId").on("input", function(){
var searchTerm = $("#searchInputId").val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function () {
$("#searchInputId").data('typeahead').source = getElasticsearchSearchResultsArray();
});
});
elasticsearchMathPhrasePrefixSearch() function
function elasticsearchMathPhrasePrefixSearch(client, searchPhrase, callback) {
console.log("Searching for: " + searchPhrase);
client.search({
body: {
"query": {
"match_phrase_prefix": {
"accountName": searchPhrase
}
}
}
}, function (error, response) {
if (error) {
console.trace('ELASTICSEARCH: Search query failed');
} else {
console.log('ELASTICSEARCH: Search query OK');
var doc = response.hits.hits;
elasticsearchSearchResultsArray = getDocs(doc);
}
callback();
});
}
getDocs() function
function getDocs(doc){
var searchResultsArray=[];
for(var i = 0; i < doc.length; i++){
searchResultsArray.push(doc[i]._source.accountName);
}
return searchResultsArray;
getElasticsearchSearchResultsArray() function
function getElasticsearchSearchResultsArray(){
return elasticsearchSearchResultsArray;
}
elasticsearchSearchResultsArray is a global array that holds the results. Because of the JS async nature I had no other idea to make it work.
EDIT:
Ok, I modified my code so the source is updated correctly with help of this Issue #1997. But now I have got another problem. The typeahead dropdown is not displayed when I type.
My new code:
var empty = [];
$("#searchInputId").typeahead({ source:empty, items:10, fitToElement:true });
$("#searchInputId").on("keyup", function(ev){
ev.stopPropagation();
ev.preventDefault();
//filter out up/down, tab, enter, and escape keys
if( $.inArray(ev.keyCode,[40,38,9,13,27]) === -1 ){
var self = $(this);
//set typeahead source to empty
self.data('typeahead').source = [];
//active used so we aren't triggering duplicate keyup events
if( !self.data('active') && self.val().length > 0){
self.data('active', true);
//Do data request. Insert your own API logic here.
var searchTerm = self.val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function() {
//set this to true when your callback executes
self.data('active',true);
//set your results into the typehead's source
self.data('typeahead').source = getElasticsearchSearchResultsArray();
//trigger keyup on the typeahead to make it search
self.trigger('keyup');
//All done, set to false to prepare for the next remote query.
self.data('active', false);
});
}
}
});
Try to use on("keyup") instead. input is 1 character behind.
Ok, I resolved it myself. I switched from bootstrap3-typeahead to jQuery UI Autocomplete. It is working great and the script is much smaller.
New code:
$("#searchInputId").on("keydown", function () {
$("#searchInputId").autocomplete({
source: function(request, response) {
var searchTerm = $("#searchInputId").val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function (){
response(getElasticsearchSearchResultsArray());
});
}
});
});
I am trying to use CSSTransitionGroup to animate Sentences in a SentenceList. When the "next" button is pressed I want the next Sentence to animate in and the 1st in the list to fadeout. However I get this error message:
Each child in an array should have a unique "key" prop. Check the
render method of Sentence.
I don't understand why that is since when I push Sentence into my List I am passing it a {sentence.id} as a "key" prop. Shouldn't React know that each sentence key is defined as such when rendering it?
I've tried defining the key again in the Sentence render method but to no avail. Are my State changes making React lose track of the current Sentence key?
Thanks for your help!
SentenceList:
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var SentenceList = React.createClass({
getInitialState: function() {
return {
sentences: this.props.sentences
}
},
//receives sentence and new blip from Sentence
addBlip: function(sentence, value) {
//see where in the loaded sentences we are
var i = this.state.sentences.indexOf(sentence),
sentences = this.state.sentences,
// callback within a callback (post), the context changes inside the callback so we need to set this to self
self = this;
$.post(
'/sentences/' + sentence.id + '/blips',
{blip: {body: value}},
//set sentence we blipped into as answered
//reset state to reload sentences state after post
function(response) {
sentences[i].answered = true;
// sentences[i].statistics = response.statistics;
// put dummy content first then work it out in the backend to receive the format you want to receive (better to work from front to back)
sentences[i].statistics = [
{word: "butts", frequency: "95%"},
{word: "dogs", frequency: "2%"},
{word: "vegetables", frequency: "1%"},
{word: "sun", frequency: "2%"}
];
self.setState({sentences: sentences});
});
},
//take in a sentence (sent from Sentence) and find current position in loaded sentences and set it to dismissed, then reload list
dismissSentence: function(sentence) {
var i = this.state.sentences.indexOf(sentence),
sentences = this.state.sentences;
sentences[i].dismissed = true;
this.setState({sentences: sentences});
},
//list undismissed sentences and take out the first 3 for display
topThreeRemainingSentences: function() {
var unanswered = _.where(this.state.sentences, {dismissed: false});
return unanswered.slice(0, 3);
},
render: function() {
var remaining = this.topThreeRemainingSentences(),
sentences = [],
index = 0;
//loop through sentences until we have 3 remaining sentences loaded
while (index <= (remaining.length - 1)) {
var sentence = remaining[index];
sentences.push(
<Sentence key={sentence.id}
isActive={index == 0}
isNext={index == 1}
isNnext={index == 2}
onDismiss={this.dismissSentence}
onSubmitBlip={this.addBlip}
details={sentence} />
)
index = index + 1;
}
return (
<ReactCSSTransitionGroup transitionName="animate">
<div>{sentences}</div>
</ReactCSSTransitionGroup>
)
}
});
Sentence:
var Sentence = React.createClass({
getDefaultProps: function() {
return {
onSubmitBlip: function() { console.log(arguments) }
}
},
//pass sentence and new blip to submit function
addBlip: function(e) {
e.preventDefault();
var blipBody = this.refs.newBlip.getDOMNode().value
this.props.onSubmitBlip(this.props.details, blipBody);
},
//send sentence to List to set it to dismissed
dismissSentence: function(e) {
e.preventDefault();
this.props.onDismiss(this.props.details);
},
render: function() {
var phrase = this.props.details.body,
phrase_display = phrase.split("*"),
before = phrase_display[0],
after = phrase_display[1],
positionClass,
stats;
if (this.props.isActive) {
positionClass = "active-sentence"
} else if (this.props.isNext) {
positionClass = "next-sentence"
} else if (this.props.isNnext) {
positionClass = "nnext-sentence"
}
//find stats for sentence if answered from json and push them into array ["word", x%]
if (this.props.details.answered) {
var words = [];
this.props.details.statistics.forEach(function(statistic) {
words.push(<li className="stats-list"><span className="stats-list-word">{statistic.word} </span>
<span className="stats-list-percent">{statistic.frequency} </span> </li>)
})
stats = <div><span className="stats-list-header">others said:</span> {words}</div>
}
if (this.props.isActive) {
nextButton = <div className="next-button" onClick={this.dismissSentence}>V</div>
}
if (this.props.isNext) {
nextButton = <div></div>
}
if (this.props.isNnext) {
nextButton = <div></div>
}
return (
<div className={"blipForm " + positionClass}>
{before}
<form onSubmit={this.addBlip}>
<input type="text"
ref="newBlip" />
</form>
{after}
{nextButton}
<br/>
<ul>{stats}</ul>
</div>
)
}
});
The <li> elements created in the Sentence component's render method need key attributes:
this.props.details.statistics.forEach(function(statistic) {
words.push(<li className="stats-list" key={statistic.id}>...</li>);
});
I want to use a select x-editable in my Meteor application. My goal is to assign users to groups. This should be reactive, so when you assign a user, other clients should see the changes. The current problem is that the assignment works (data-value changes), but only the user who made the change is able to see the new value.
Here is my code:
Template.userGroup.rendered = function() {
var groupId = this.data._id;
var sourceUsers = [];
Users.find().forEach(function(user) {
sourceUsers.push({value: user._id, text: user.username});
});
Tracker.autorun(function() {
$('.assign-user').editable("destroy").editable({
emptytext: "Empty",
source: sourceUsers,
success: function(response, result) {
if (result) {
Groups.update({_id: groupId}, {$set: {adminId: result}});
}
}
});
});
};
<template name="userGroup">
</template>
I already tried to "destroy" the stale x-editable and put it inside the Tracker.autorun function, but unfortunately, this does not work.
Any help would be greatly appreciated.
I don't use Tracker.autorun but I use x-editable for inline editing like this:
(also used it for group assigments - just like your case, but found it too clumsy on the UI side). Anyway, here's my code:
Template
<template name="profileName">
<td valign='top'>
<div id="profileNameID" class="editable" data-type="text" data-rows="1">{{profile.name}}</div>
</td>
</template>
And on the JS side
Template.profileName.rendered = function () {
var Users = Meteor.users;
var container, grabValue, editableColumns, mongoID,
_this = this;
var container = this.$('#profileNameID');
var editableColumns = container.size();
grabValue = function () {
var gValue = $.trim(container.html());
return gValue;
};
$.fn.editable.defaults.mode = 'inline';
return container.editable({
emptytext: 'Your name goes here',
success: function (response, newValue) {
var mongoID = removeInvisibleChars($(this).closest("tr").find(".mongoid").text());
var editedUser = _users.findOne({
_id: mongoID
});
Meteor.users.update(mongoID, {
$set: {
"profile.name": newValue
}
});
return container.data('editableContainer').formOptions.value = grabValue;
}
});
Update happens immediately on all subscribed authorized clients.
I have an object that is constructed upon a table row from the database. It has all the properties that are found in that entry plus several ko.computed that are the middle layer between the entry fields and what is displayed. I need them to be able translate foreign keys for some field values.
The problem is the following: One of the properties is an ID for a string. I retrieve that ID with the computed. Now in the computed will have a value that looks like this: 'option1|option2|option3|option4'
I want the user to be able to change the options, add new ones or swap them around, but I also need to monitor what the user is doing(at least when he adds, removes or moves one property around). Hence, I have created an observable array that I will bind in a way that would allow me to monitor user's actions. Then the array will subscribe to the computed so it would update the value in the database as well.
Some of the code:
function Control(field) {
var self = this;
self.entry = field; // database entry
self.choices = ko.observableArray();
self.ctrlType = ko.computed({
read: function () {
...
},
write: function (value) {
if (value) {
...
}
},
owner: self
});
self.resolvedPropriety = ko.computed({
read: function () {
if (self.ctrlType()) {
var options = str.split('|');
self.choices(createObservablesFromArrayElements(options));
return str;
}
else {
return '';
}
},
write: function (value) {
if (value === '') {
//delete entry
}
else {
//modify entry
}
},
deferEvaluation: true,
owner: self
});
self.choices.subscribe(function (newValue) {
if (newValue.length !== 0) {
var newStr = '';
$.each(newValue, function (id, el) {
newStr += el.name() + '|';
});
newStr = newStr.substring(0, newStr.lastIndexOf("|"));
if (self.resolvedPropriety.peek() !== newStr) {
self.resolvedPropriety(newStr);
}
}
});
self.addChoice = function () {
//user added an option
self.choices.push({ name: ko.observable('new choice') });
};
self.removeChoice = function (data) {
//user removed an option
if (data) {
self.choices.remove(data);
}
};
...
}
This combination works, but not as I want to. It is a cyclic behavior and it triggers too many times. This is giving some overload on the user's actions because there are a lot of requests to the database.
What am I missing? Or is there a better way of doing it?
Quote from knockout computed observable documentation
... it doesn’t make sense to include cycles in your dependency chains.
The basic functionality I interpreted from the post:
Based on a field selection, display a list of properties/options
Have the ability to edit said property/option
Have the ability to add property/option
Have the ability to delete property/option
Have the ability to sort properties/options (its there, you have to click on the end/edge of the text field)
Have the ability to save changes
As such, I have provided a skeleton example of the functionality, except the last one, you described #JSfiddle The ability to apply the changes to the database can be addressed in several ways; None of which, unless you are willing to sacrifice the connection overhead, should include a computed or subscription on any changing data. By formatting the data (all of which I assumed could be collected in one service call) into a nice nested observable view model and passing the appropriate observables around, you can exclude the need for any ko.computed.
JS:
var viewModel = {
availableFields : ko.observableArray([
ko.observable({fieldId: 'Field1',
properties: ko.observableArray([{propertyName: "Property 1.1"}])}),
ko.observable({fieldId: 'Field2',
properties: ko.observableArray([{propertyName:"Property 2.1"},
{propertyName:"Property 2.2"}])})]),
selectedField: ko.observable(),
addProperty: function() {
var propertyCount = this.selectedField().properties().length;
this.selectedField().properties.push({propertyName: "Property " + propertyCount})
},
};
ko.applyBindings(viewModel);
$("#field-properties-list").sortable({
update: function (event, ui) {
//jquery sort doesnt affect underlying array so we have to do it manually
var children = ui.item.parent().children();
var propertiesOrderChanges = [];
for (var i = 0; i < children.length; ++i) {
var child = children[i];
var item = ko.dataFor(child);
propertiesOrderChanges.push(item)
}
viewModel.selectedField().properties(propertiesOrderChanges);
}
});
HTML:
<span>Select a field</span>
<select data-bind='foreach: availableFields, value: selectedField'>
<option data-bind='text: $data.fieldId, value: $data'></option>
</select>
<div style="padding: 10px">
<label data-bind='text: "Properties for " + selectedField().fieldId'></label>
<button data-bind='click: $root.addProperty'>Add</button>
<ul id='field-properties-list' data-bind='foreach: selectedField().properties'>
<li style = "list-style: none;">
<button data-bind="click: function() { $root.selectedField().properties.remove($data) }">Delete</button>
<input data-bind="value: $data.propertyName"></input>
</li>
</ul>
</div>