Search and Sort on the same page - meteor

I'm trying to implement sort and search to my items, so i started with sort and it works:
Template
<button class="sort">Sort</button>
{{#each cvs}}
{{> Interviu}}
{{/each}}
JS:
Template.Interviuri.onCreated(function () {
var self = this
self.autorun(function () {
self.sortOrder = new ReactiveVar(-1)
})
Template.Interviuri.helpers({
cvs() {
const instance = Template.instance()
return Cvs.find({}, { sort: { createdAt: instance.sortOrder.get() } })
},
})
Template.Interviuri.events({
'click .sort'(event, instance) {
instance.sortOrder.set(instance.sortOrder.get() * -1)
Next i wanted to implement Search on the same page. So the best way i could found was EasySearch.
But using EasySearch, it means i must change the way my items are being displayed. And then the sort doesn't work anymore.
Template
<div class="searchBox pull-right">
{{> EasySearch.Input index=cvsIndex attributes=searchAttributes }}
</div>
{{#EasySearch.Each index=cvsIndex }}
{{> Interviu}}
{{/EasySearch.Each}}
Collection
CvsIndex = new EasySearch.Index({
collection: Cvs,
fields: ['name'],
engine: new EasySearch.Minimongo()
})
JS
cvsIndex: () => CvsIndex,
How can i have both search and sort working at the same time?

With EasySearch you can use two methods on your index, namely getComponentDict() and getComponentMethods().
With getComponentDict() you can access search definition and options:
index.getComponentDict().get('searchDefinition');
index.getComponentDict().get('searchOptions');
You also have the corresponding setters to change the search definition/option.
getComponentMethods has mehods like
index.getComponentMethods().loadMore(integer);
index.getComponentMethods().hasMoreDocuments();
index.getComponentMethods().addProps(prop, value);
index.getComponentMethods().removeProps([prop])
From that you can set your prop, say index.getComponentMethods().addProp('sort', -1) and then on the index definition, in your MongoDB engine, set the sort from that prop:
index = new EasySearch.index({
// other parameters
engine: new EasySearch.MongoDB({
sort: function(searchObject, options) {
if(options.search.props.sort) {
return parseInt(options.search.props.sort);
}
return 1;
}
})
});
See EasySearch Engines for more info.

Related

rendering alternate components under the same flow:router URL

How can I render two different views in a one page app without changing URLs. I'm using meteor with the default blaze as well as the flow:router package. Right now I have it set up like this:
routes.js..
FlowRouter.route("/", {
name: "App.home",
action() {
BlazeLayout.render("App_body", {
main: "App_home",
mainContent: "calendar"
});
}
});
FlowRouter.route("/list", {
name: "App.list",
action() {
BlazeLayout.render("App_body", { main: "App_home", mainContent: "list" });
}
});
but this way I'm using the url /list and i dont want that. I would like to simply render an alternate component template in the same url. I'm very new to coding so forgive me if this is obvious. Essentially I just want two different view styles: a list and a calendar. So I would like a way to set it up so that a spacebars template can be rendered if a certain button is clicked, and a different one can be rendered instead if another button is clicked.
Thanks so much for any help, i've been at this for a couple of days :)
Create another template, which renders particular view conditionally. Something like this:
FlowRouter.route("/", {
name: "App.home",
action() {
BlazeLayout.render("App_body", {
main: "App_home",
mainContent: "listOrCal"
});
}
});
<template name="listOrCal">
{{#if showList}}
{{> list}}
{{else}}
{{> calendar}}
{{/if}}
<button id="switchView">Switch view</button>
</template>
Template.listOrCal.onCreated(function listOrCalOnCreated() {
this.showList = new ReactiveVar(true);
})
Template.listOrCal.helpers({
showList() {
return Template.instance().showList.get();
}
})
Template.listOrCal.events({
'click #switchView' {
let showList = Template.instance().showList.get();
Template.instance().showList.set(!showList);
}
})
You can handle this within a single template like so:
FlowRouter.route('/', {
name: 'App.home',
action() {
BlazeLayout.render('App_body', { main: 'App_home', mainContent: 'ListOrCalendar' });
}
And then the ListOrCalendar template would look like this:
{{#if displayList}}
{{> List}}
{{else}}
{{> Calendar}}
{{/if}}
<button>Switch</button>
You would set up a ReactiveVar in the ListOrCalendar template:
Template.ListOrCalendar.onCreated(function() {
const instance = this;
instance.displayList = new ReactiveVar(true);
});
See ReactiveVar explanation here (ignore Session)
Then you would have a helper which returns the value of your ReactiveVar:
Template.ListOrCalendar.helpers({
displayList() {
const instance = Template.instance();
return instance.displayList.get();
}
});
Finally, you would hook up an event to change the value of displayList to switch between templates:
Template.ListOrCalendar.events({
"click button"(event, instance) {
const displayListCurrent = instance.displayList.get();
const displayListNew = !displayListCurrent;
instance.displayList.set(displayListNew);
// or, more concisely, instance.displayList.set(!instance.displayList.get());
}
});
So, in summary:
When the template is created, your ReactiveVar is true
so your displayList returns true
so the #if displayList condition in the template is satisfied
and so the List template is displayed
When the button is clicked
The ReactiveVar is set to false
so the displayList helper returns false
so the #if displayList condition in the template is not satisfied and it goes to the else statement
and so, finally, the Calendar template is displayed
When the button is clicked again, the ReactiveVar is toggled back to true, and on we go as above
This might seem daunting or over-complicated, but there's nothing fancy going on here at all. You'll get used to it pretty quickly

Meteor with ViewModel package not updating accross multiple child templates

I am new to meteor, and have a basic understanding of what is going on, but I am stuck with this example (the problem has been simplified as much as possible):
I have a template, and a child template:
<template name="test">
{{#each items}}
{{> testItem}}
{{/each}}
{{#each items}}
{{> testItem}}
{{/each}}
</template>
<template name="testItem">
<div {{ b "click: toggle"}}>{{value}}</div>
</template>
Template.test.viewmodel({
items: [],
onCreated: function() {
this.items().push({ value: 'test' });
}
})
Template.testItem.viewmodel({
toggle: function() {
this.value("changed");
}
});
The thing here is we have a single array of items in the viewmodel, and we render it through a child template multiple times.
When we toggle the item, it only toggles the single item template, not the other representation of it. It is behaving like it is copying the value, or some sort of scoping is taking place.
My expectation would be the second item to also change, but this is not the case - what am I missing, or misunderstanding here?
EDIT - Additional Investigation
If I change the item through the parent, and notify it has changed, the changes propogate throughout the child templates
Template.testItem.viewmodel({
toggle: function () {
this.parent().items()[0].value = "changed";
this.parent().items().changed();
}
});
Thanks!
You're right, when you do this.value("changed"); you're changing the value of the testItem view model, not the parent array. If you're going to modify the properties of objects in an array I highly recommend you use a client side Mongo collection instead. It will save you a lot of headaches.
Items = new Mongo.Collection(null);
Template.test.viewmodel({
items: function() {
return Items.find();
},
onCreated: function() {
Items.insert({ _id: "1", value: 'test' });
}
})
Template.testItem.viewmodel({
toggle: function() {
Items.update({ _id: this._id() }, { value: 'changed' });
}
});
btw, I rarely check SO. You will get quicker responses on viewmodelboard.meteor.com

Meteor Autoform - Prevent Field from Update

In my autoform the value of a field is the difference of two other input fields. It is not allowed to be updated by the user. Unfortuantly at the moment it is not possible to set a single field to readonly in a form. So my approach is to create an autoValue and a custom Validation to prevent an update
My code so far:
'SiteA.Settings.RXSignalODU1difference': {
type: Number,
label: "RX Signal [dBm] ODU1 difference (without ATPC +/- 3dbm)",
decimal: true,
autoform: {
type: "number"
},
autoValue: function() {
var ODU1gemessen = this.field("SiteA.Settings.RXSignalODU1");
var ODU1planned = this.field("SiteA.Settings.RXSignalODU1planned");
if (ODU1gemessen.isSet || ODU1planned.isSet) {
return ODU1gemessen.value - ODU1planned.value;
}
},
custom: function() {
var ODU1gemessen = this.field("SiteA.Settings.RXSignalODU1");
var ODU1planned = this.field("SiteA.Settings.RXSignalODU1planned");
var dif = ODU1gemessen.value - ODU1planned.value;
if (this.value !== dif) {
return "noUpdateAllowed";
}
}
},
My Simple.Schema message:
SimpleSchema.messages({noUpdateAllowed: "Can't be updated"});
Unfortunatly no message pops up.
EDIT
This method will create a disabled input box within your form that will automatically show the difference between two other input fields as the user types.
First, we define session variables for the values used in the calculation and initialize them to undefined.
Template.xyz.onRendered({
Session.set("ODU1gemessen", undefined);
Session.set("ODU1planned", undefined);
});
Then we define two events, that will automatically update these session variables as the user types.
Template.xyz.events({
'keyup #RXSignalODU1' : function (event) {
var value = $(event.target).val();
Session.set("ODU1gemessen", value);
},
'keyup #RXSignalODU1planned' : function (event) {
var value = $(event.target).val();
Session.set("ODU1planned", value);
}
});
Then we define a helper to calculate the difference.
Template.xyz.helpers({
RXSignalODU1difference : function () {
var ODU1gemessen = Session.get("ODU1gemessen");
var ODU1planned = Session.get("ODU1planned");
if (!!ODU1gemessen || !!ODU1planned) {
return ODU1gemessen - ODU1planned;
}
}
});
My HTML markup looks like this. Note, to still control the order of the form, I use a {{#autoform}} with a series of {{> afQuickfields }} rather than using {{> quickForm}}.
To display the calculated difference, I just create a custom div with a disabled text box.
<template name="xyz">
{{#autoForm collection="yourCollection" id="yourId" type="insert"}}
<fieldset>
<legend>Enter legend text</legend>
{{> afQuickField name="SiteA.Settings.RXSignalODU1" id="RXSignalODU1"}}
{{> afQuickField name="SiteA.Settings.RXSignalODU1planned" id="RXSignalODU1planned"}}
<div class="form-group">
<label class="control-label">RXSignalODU1difference</label>
<input class="form-control" type="text" name="RXSignalODU1difference" disabled value="{{RXSignalODU1difference}}">
<span class="help-block"></span>
</div>
</fieldset>
<button class="btn btn-primary" type="submit">Insert</button>
{{/autoForm}}
</template>
Original Answer - not recommended
If you are generating your form as a quickForm, you could do something like
{{>quickForm collection='yourCollection' omitFields='SiteA.Settings.RXSignalODU1difference'}}
This will leave this field off the form, so the user won't be able to update it.
If you still want to display the value somewhere along with the form as the user types in the other two values, you could define a helper in your client side js
something like
Template.yourFormPage.helpers({
diff: function () {
var ODU1gemessen = $('[name=SiteA.Settings.RXSignalODU1]').val();
var ODU1planned = $('[name=SiteA.Settings.RXSignalODU1planned]').val();
if (!!ODU1gemessen || !!ODU1planned) {
return ODU1gemessen - ODU1planned;
}
}
});
You'll want to double check how the field names are being rendered in your DOM. Autoform assigns the name attribute using the field names in your schema, but I don't know how it handles nested keys... (i.e. whether it names the element 'SiteA.Settings.RXSignalODU1' or just 'RXSignalODU1' )
And then just display the value somewhere in your html as :
{{diff}}

this.findAll not working on sub-template

When I try to use this.findAll on a template where the selector is in a sub-template, findAll returns nothing.
Here's the HTML:
<template name="products">
{{#each productList}}
{{> product }}
{{/each}}
</template>
<template name="product">
<div class="box">{{name}}</div>
</template>
Here's the JS:
Template.products.helpers({
productList: function() {
var all = Products.find({}).fetch();
return all;
}
});
Template.products.rendered = function(){
var boxes = this.findAll('.box');
console.log(boxes.length);
}
Output of boxes.length is 0. Any ideas how I could get the "box" elements?
According to the docs for findAll:
Only elements inside the template and its sub-templates can match parts of the selector.
So it should work for sub-templates. I tried this with a fixed array of products and it worked, which implies that you are just seeing a delay between the call to rendered and the products being fetched. For example if you do:
Template.products.events({
'click .box': function (e, t) {
var boxes = t.findAll('.box');
console.log(boxes.length);
}
});
Then if you click on one of the boxes, you should see the correct number logged to the console. In short, I think the test may just be invalid. If you are using iron-router, you could try adding a waitOn for the products - that may ensure they arrive before the rendered call.
Here's what I did to run a script after all products have been loaded.
I've added last_product property in all the products.
Template.products.helpers({
productList: function() {
var all = Products.find({}).fetch();
var total = all.length;
var ctr = 0;
all.forEach(function(doc){
doc.last_product = false;
ctr++;
if(ctr == total)
{
doc.last_product = true;
}
return doc;
});
return all;
}
});
Then instead of "Template.products", I used "Template.product" to detect if the last product is rendered. When the last product is rendered, run the script.
Template.product.rendered = function(){
if(this.data.last_product){
var boxes = $('.pbox');
console.log(boxes.length);
}
}
boxes.length now has the correct length.
Thanks to David for the idea!
Here's the correct answer. I've added this to my iron-router route:
action : function () {
if (this.ready()) {
this.render();
}
}
Found the answer from https://stackoverflow.com/a/23576039/130237 while I was trying to solve a different problem.

Meteor Group Collection by Field

I am trying to return a collection (Postings) grouped by a field (status). I am pretty new to mongo and meteor. The query below gives me the collections grouped by status with # of docs by that status... basically I want the same thing but have the actual documents in there.
Also, I would like to be able to publish/subscribe to this so that they reactivly update. I am creating an admin dashboard that groups all the Postings by current status.
A friend provided the following gist, but it is a bit over my head: https://gist.github.com/ryw/8827179
db.postings.group({ key: {status: 1}, initial: {sum:0}, reduce: function(doc, prev) { prev.sum += 1; } })
Thanks!
If you need all of the documents on the client, then I would just publish the whole collection and let the template code group them.
client
Tracker.autorun(function() {
if (Meteor.user()) {
Meteor.subscribe('allPostings');
}
});
Template.admin.helpers({
postings: function() {
if (Session.get('currentStatus')) {
return Postings.find({status: Session.get('currentStatus')});
}
},
statuses: function() {
return _.uniq(_.pluck(Postings.find().fetch(), 'status'));
}
});
Template.admin.events({
'click .status': function() {
Session.set('currentStatus', String(this));
}
});
<template name="admin">
<div class="left-panel">
<ul>
{{#each statuses}}
<li class="status">{{this}}</li>
{{/each}}
</ul>
</div>
<div class="right-panel">
<ul>
{{#each postings}}
<li>{{message}}</li>
{{/each}}
</ul>
</div>
</template>
server
Meteor.publish('allPostings', function() {
var user = Meteor.users.findOne(this.userId);
if (user.isAdmin) {
return Postings.find();
}
});
I'm assuming you have some way to identify admin users (here I used isAdmin). I am also assuming that a posting has a status and a message.
Instead of using aggregate functions or map reduce operations, you could denormalize your data and store a separate collection of the groups and their counts.
You can update your counts using observe functions as in the following example from the relevant section of meteor docs:
// Keep track of how many administrators are online.
var count = 0;
var query = Users.find({admin: true, onlineNow: true});
var handle = query.observeChanges({
added: function (id, user) {
count++;
console.log(user.name + " brings the total to " + count + " admins.");
},
removed: function () {
count--;
console.log("Lost one. We're now down to " + count + " admins.");
}
});
// After five seconds, stop keeping the count.
setTimeout(function () {handle.stop();}, 5000);
This way, you can present the groups and their counts on a template and it would be reactive.

Resources