I have a search form with a number of empty fields. After the user enters values into the fields and hits "Enter" I create a query string from the values (using $.param()) and send that to the server. All is well, until you empty a field and hit "Enter" again. Because that field previously had a value the attribute has already been set, so now when hitting submitting the attribute is still being added to the query string, only with no value attached.
An example, and some code:
Search.CustomerCard = Marionette.ItemView.extend({
template: "#customer-search-card",
tagName: "section",
className: "module primary-module is-editing",
initialize: function () {
this.model.on('change', function(m, v, options) {
console.log("Changed?", m.changedAttributes();
});
},
events: {
"click .js-save": "searchCustomers",
"keypress [contenteditable]": "searchCustomersOnEnter"
},
bindings: {
…
},
onRender: function () {
this.stickit();
},
searchCustomersOnEnter: function (e) {
if (e.keyCode !== 13) { return; }
e.preventDefault();
this.searchCustomers(e);
},
searchCustomers: function (e) {
e.preventDefault();
$(e.target).blur();
var query = $.param(this.model.attributes);
this.trigger("customers:search", query);
}
});
You'll see in the var query = $.param(this.model.attributes); line that I'm converting the model's attributes to a query string to send along to my API. You may also see I'm using backbone.stickit. How can I seamlessly unset any attributes that are empty before creating the query string?
In this scenario I'd just fix the input to $.param:
var params = _.clone(this.model.attributes);
_.each(params, function(value, key) {
if (value == "") {
delete params[key];
}
});
$.param(params);
There seems to be no proper utility available at Underscore, but could be easily provided similar to what is described here.
I ran into a similar problem when i was making my project, for me _.defaults worked, but maybe in your case, you can use one of filter, reduce or anyother underscore functions.
Related
I am trying to findOne document in my Template.admin.events code. I have a form and onClick I want to verify if the ID of the ObjectID entered is an existing document in my collection and fetch that result to show it on the template.
My event code on the client :
Template.admin.events({
'click #btnAjouterObjet'(event) {
let objetIdInput = $('#object_id').val().toString();
Meteor.subscribe('objetsFindOne', objetIdInput, {
onReady: function () {
let obj = Objets.findOne();
if (obj) {
console.log("found");
console.log(obj);
objetsArr.push(objetIdInput);
}
else {
console.log("not found");
console.log(obj);
}
}
});
}
});
In my Objets api :
Meteor.publish('objetsFindOne', function objetsFindOne(param_id){
return Objets.find({_id : param_id});
})
I have verified and my objetIdInput always change on click when a different Id is entered but the subscribe always returns the first id entered. I also added the onReady because otherwise it returned undefined.
I am new to Meteor and I have also tried to subscribe to all the collection and doing the find on the client but I don't think it is the best idea as my collection has about 22000 documents.
Just to elaborate a little bit on the first answer, as to how to change this pattern:
(1) you should place your Meteor.subscribe() call in your Template.admin.onCreated() function.
(2) the subscription reads from a reactive value, for example, new ReactiveVar().
(3) now, anytime the reactive value changes, the subscription will re-run. So, in your template event, you set the reactive value to the id and let the subscription handle the rest.
Discover Meteor and other resources should be helpful on any details.
You are going about this all wrong. I suggest you take a look at Template-Level Subscriptions
I opted for the use of a method :
Client side:
'click #btnAjouterObjet'(event) {
let objetIdInput = $('#object_id').val().toString();
let result = Meteor.call('findObj', objetIdInput, function (error, result) {
if (error) {
console.log(error.reason);
return;
}
console.log(result);
});
}
On the server side :
Meteor.methods({
findObj: function (param_id) {
console.log(Objets.find({ _id: param_id }).fetch());
return Objets.find({ _id: param_id }).fetch();
},
});
I am building what should be a fairly simple project which is heavily based on Ampersand's starter project (when you first run ampersand). My Add page has a <select> element that should to be populated with data from another collection. I have been comparing this view with the Edit page view because I think they are quite similar but I cannot figure it out.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
Below you can see that I am trying to fetch the app.brandCollection and set its value to this.model, is this correct? I will need to modify the output and pass through the data to an ampersand-select-view element with the correct formatting; that is my next problem. If anyone has suggestions for that I would also appreciate it.
var PageView = require('./base');
var templates = require('../templates');
var ProjectForm = require('../forms/addProjectForm');
module.exports = PageView.extend({
pageTitle: 'add project',
template: templates.pages.projectAdd,
initialize: function () {
var self = this;
app.brandCollection.fetch({
success : function(collection, resp) {
console.log('SUCCESS: resp', resp);
self.brands = resp;
},
error: function(collection, resp) {
console.log('ERROR: resp', resp, options);
}
});
},
subviews: {
form: {
container: 'form',
waitFor: 'brands',
prepareView: function (el) {
return new ProjectForm({
el: el,
submitCallback: function (data) {
app.projectCollection.create(data, {
wait: true,
success: function () {
app.navigate('/');
app.projectCollection.fetch();
}
});
}
});
}
}
}
});
This is only the add page view but I think that is all that's needed.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
This string represents path in a current object with fixed this context. In your example you've waitFor: 'brands' which is nothing more than PageView.brands here, as PageView is this context. If you'd have model.some.attribute, then it'd mean that this string represents PageView.model.some.attribute. It's just convenient way to traverse through objects.
There's to few informations to answer your latter question. In what form you retrieve your data? What do you want to do with it later on?
It'd be much quicker if you could ping us on https://gitter.im/AmpersandJS/AmpersandJS :)
I'm using Backbone.js with backbone.validation plugin to build a custom validator that checks if the email address (entered in a form input) is already taken.
The new validator is called emailAvailable and you can see below:
(notice: it's Coffescript, but at the bottom you'll find the code converted to standard javascript)
# ==================================
# MODELS
# ==================================
User = Backbone.Model.extend(
urlRoot: "/user"
validation:
email:
fn: "emailAvailable"
emailAvailable: (value, attr, computedState) ->
// Ajax call to server (Play framework 2.2.1): returns the string "email available" if it doesn't find the email and returns the email address if it find it
checkEmail = $.ajax(jsRoutes.controllers.Signup.isEmailExists(value))
checkEmail.done (msg) ->
emailFound = msg
if value is emailFound
return "already taken"
return
)
# ==================================
# VIEWS
# ==================================
SignUpView = Backbone.View.extend(
initialize: ->
Backbone.Validation.bind(this)
el: "body"
events:
"change input" : "validateInput"
validateInput: (event) ->
input = $(event.currentTarget)
inputName = event.currentTarget.name
inputValue = input.val()
this.model.set(inputName, inputValue)
if this.model.isValid(inputName)
input.removeClass "error"
input.addClass "valid"
else
input.removeClass "valid"
input.addClass "error"
...
This doesn't work and I can't get why. Where am I wrong?
EDIT: code converted to javascript
var SignUpView, User;
User = Backbone.Model.extend({
urlRoot: "/user",
validation: {
email: {
fn: "emailAvailable"
}
},
emailAvailable: function(value, attr, computedState) {
var checkEmail;
checkEmail = $.ajax(jsRoutes.controllers.Signup.isEmailExists(value));
checkEmail.done(function(msg) {
var emailFound;
emailFound = msg;
if (value === emailFound) {
return "already taken";
}
});
}
});
SignUpView = Backbone.View.extend({
initialize: function() {
return Backbone.Validation.bind(this);
},
el: "body",
events: {
"change input": "validateInput"
},
validateInput: function(event) {
var input, inputName, inputValue;
input = $(event.currentTarget);
inputName = event.currentTarget.name;
inputValue = input.val();
this.model.set(inputName, inputValue);
if (this.model.isValid(inputName)) {
input.removeClass("error");
return input.addClass("valid");
} else {
input.removeClass("valid");
return input.addClass("error");
}
}
});
Backbone.Validation sadly doesn't support asynchronous validation functions. This is basically limitation of default backbone validation flow. It was designed with only synchronous way of validation in mind.
You have basically 2 options:
specify async:false option for ajax call
implement your own validation flow for this case
I personally would go with option 2, since synchronous ajax calls will lock browser until call is completed.
Update note:
I did quick google search after I answered this question, and it looks like there is extension for Backbone.Validation, which allows asynch validation. Please note, that I haven't used, nor tested it in any way :)
Link: https://github.com/suevalov/async.backbone.validation
You will have to do your async validation function yourself. Here is how you can do it without any plugins, just a little logic and good old coding.
1) Let's start where your function that will save your model is. You will need a Deferred object, like this, before saving happens:
this.asyncValidation = $.Deferred();
2) Then you will have to manually call your own custom validate function:
this.model.asyncValidate();
3) Then you will have to wait until your async validation is done. Once it's done, just save your model:
$.when(this.checkDuplicatesFinished).done(function () {
4) Check your own model attribute that has the result of your own async validation:
if (self.model.asyncValidateOK) {
5) Save your model:
self.model.save();
Here is the code altogether:
this.asyncValidation = $.Deferred();
this.model.asyncValidate(asyncValidation);
var self = this;
$.when(this.checkDuplicatesFinished).done(function () {
if (self.model.asyncValidateOK) {
self.model.save();
}
});
Now, let's see your Model. Here is where the new custome validate function is going to be located. It's pretty much straightforward, you will need a boolean variable to store the result of the validation and the validate method itself.
window.app.MyModel = Backbone.Model.extend({
asyncValidateOK: true,
asyncValidate: function (defferedObject) {
var self = this;
var attrs = this.attributes; // a copy of the params that are automatically passed to the original validate method
$.get("someAsyncMethod", {}, function(result){
// do your logic/validation with the results
// and tell the referred object you finished
if (true){ //validation ok (use your own logic)
self.asyncErrors = []; // no errors
self.asyncValidateOK = true;
deferredObject.resolve();
} else { // validation not ok
self.asyncErrors.push();
self.asyncValidateOK = false;
deferredObject.resolve();
}
});
}
});
For more documentation check http://backbonejs.org/, but there's nothing related to this. Hope this helps anyone that ends up trying to async validate.
So I'm trying to get started with a simple meteor app to search a database. I've got a single input box from which I get the search query with the following code:
Template.search.events = {
'keydown input#search' : function (event) {
if (event.which == 13) {
var item = document.getElementById('search');
Template.results.results(item.value)
//console.log(item);
item.value = '';
}
}
}
I pass the search query to another function which is supposed to query the mongodb and print the result in the template:
Template.results.results = function (item) {
return Products.find({sku: item});
}
However, it never finds the item! If I run the same query in Chrome's console it works. If I replace {sku: item} in the code with (for example) {sku: "A2277"} (which is in my db) then it works! If i create a new variable inside the Template.results.results function such as var item = "A2277" that works too. What's going on here?!
Template helpers were designed to be called by your template, not directly by your event handlers. Your code just asks a query to happen and return a value, but it isn't tied to your template in any way. Instead, you should use a session variable like so:
Template.results.results = function() {
return Products.find({sku: Session.get('itemSku')});
};
Then in your event handler you can do something like:
Template.search.events({
'keydown input#search': function(event) {
if (event.which === 13) {
var $item = $('#search');
Session.set('itemSku', $item.val());
$item.val('');
}
}
});
Note I used jQuery here to set/get the item values. Anyway, that should set the session variable and reactively redraw the results.
I'm not experienced in Javascript but I've read a ton of articles about Meteor reactivity but still can't figure out why it is not working in my case.
When a new product is added, I want to be recalculated total cost and use it in the totalCost helper so it's almost real time visible in the browser.
Can someone please take a look at my code and try to figure out some logic error? Everything except the reactivity is working on my computer.
I have got this method in /models/Product.js :
Meteor.methods({
totalProductCost: function() {
var pipeline = [
{$match: {owner: Meteor.userId()}},
{$group: {_id: null, cost: {$sum: "$cost"}}}
];
var data = Products.aggregate(pipeline)["0"].cost;
return (data === undefined) ? 0 : data;
}
});
Then I've got layout.js in client folder:
if (Meteor.isClient) {
var handle = Meteor.subscribe("Products", Meteor.userId());
ProductManager = {
_productItems: null,
_dep: new Tracker.Dependency(),
getProducts: function () {
this._dep.depend();
return this._productItems;
},
setProducts: function (value) {
if (value !== this._productItems) {
this._productItems = value;
this._dep.changed();
}
},
getTotalCost: function () {
return ReactiveMethod.call('totalProductCost');
}
}
// TRACKER
Tracker.autorun(function () {
if (handle.ready()) {
ProductManager.setProducts(Products.find().fetch());
}
});
// HELPERS
Template.boxOverview.helpers({
"totalCost" : function () {
return ProductManager.getTotalCost();
},
});
}
It seems that you used a collection.aggregate in a method. If you need reactivity, you need to use a publication rather than a method (or you need to call the method each time you want to refresh). However, if you use your aggregation inside your publication (I assume you use a package for it) you will loose reactivity as well.
What I would advise you is to use a publication without aggregate function. You calculate your product cost by creating a new field and adding it to your cursor. Once, you do that, if you want to keep reactivity, it is necessary to use to use cursor.observeChanges() or just cursor.observe().
Have a look at this example:
var self = this;
// Modify the document we are sending to the client.
function filter(doc) {
var length = doc.item.length;
// White list the fields you want to publish.
var docToPublish = _.pick(doc, [
'someOtherField'
]);
// Add your custom fields.
docToPublish.itemLength = length;
return docToPublish;
}
var handle = myCollection.find({}, {fields: {item:1, someOtherField:1}})
// Use observe since it gives us the the old and new document when something is changing.
// If this becomes a performance issue then consider using observeChanges,
// but its usually a lot simpler to use observe in cases like this.
.observe({
added: function(doc) {
self.added("myCollection", doc._id, filter(doc));
},
changed: function(newDocument, oldDocument)
// When the item count is changing, send update to client.
if (newDocument.item.length !== oldDocument.item.length)
self.changed("myCollection", newDocument._id, filter(newDocument));
},
removed: function(doc) {
self.removed("myCollection", doc._id);
});
self.ready();
self.onStop(function () {
handle.stop();
});
This is taken from here.