What is the correct approach when working with an "new object" that is to be saved in a collection. Say I have a collection Cars. I have a /cars/new-car
url and then a form with:
name: __
parts: list of parts here
If I want to make this form "reactive" in the sense that if I add a new part in the parts array it shows a rerender is the best approach to make the whole "Car" a reactive object. Or should one just add a new row in the dom?
I dont want to automatically insert the whole thing into the "Cars" collection until It has a name and a list of parts.
Most examples shows very simple of adding to collection -> rerender of DOM which is very straightforward.
Edit: The same concept may apply to when editing a car. Fetching the car from a collection, setting up so the returned object is reactive(so I can add/remove parts) when done get all values and store the edited car information.
Start out by initializing an "empty" car as a reactive variable.
Template.cars.onCreated(function () {
this.car = new ReactiveVar({}); // empty car
});
Say your dom has some sort of attribute on each field describing which key it is:
<input data-key="name" placeholder="Car name"/>
Then you can bind an event that will use the data from this to update the reactive variable.
Template.cars.events({
'change input': function (e, template) {
template.car.set(_.extend(template.car.get(), {
[$(e.target).data('key')]: $(e.target).val()
}));
}
});
This will construct the object as you fill in your inputs.
Consider using Session for your /cars/new-car page
When the page first loads
Session.set('parts', []});
Session.set('name', '');
When the user saves a part
var addedPart = getPart();
var update = Session.get('parts').push(addedPart);
Session.set('parts', update);
Then your template helper functions can get everything it needs to render the view by calling Session.get().
Template.view.helpers({
currentParts: function() {
return Session.get('parts');
}
});
What do you think? I'm fairly new to Meteor myself, so there maybe even more clever ways to do batch updates on the session. But this is general gist.
Related
I have a simple application form. On click of one button I just need to add text fields and on click of another button, just remove text field dynamically.
How can this be done in meteor without using jQuery as I have seen many blogs that says it is not a good practice to use jQuery with meteor. Can any tell me how can this be achieved without using jQuery.
You can use a reactive variable and a helper that returns an array based on that reactive variable to construct template-level {{#each}} statements. A good choice for a reactive variable is the Session variable, since it's built into Meteor (you won't need the ReactiveVar package or to set up your own dependencies).
Then, you can use event handlers to update the reactive variable as appropriate. For example...
//client only code
Template.test.onCreated(function() {
Session.set('inputs', []); // on page load, set this to have no inputs
});
Template.test.helpers({
inputs: function () {
return Session.get('inputs'); // reactively watches the Session variable, so when it changes, this result will change and our template will change
}
});
// Now we'll set up a click handler to add inputs to our array when we click the "add" button
Template.test.events({
'click #add-input': function () {
var inputs = Session.get('inputs');
var uniqid = Math.floor(Math.random() * 100000); // Give a unique ID so you can pull _this_ input when you click remove
inputs.push({uniqid: uniqid, value: ""});
Session.set('inputs', inputs);
}
});
// We also need handlers for when the inputs themselves are changed / removed
Template.input.events({
'click .remove-input': function(event) {
var uniqid = $(event.currentTarget).attr('uniqid');
inputs = Session.get('inputs');
inputs = _.filter(inputs, function(x) { return x.uniqid != uniqid; });
Session.set('inputs', inputs);
},
'change input': function(event) {
var $input = $(event.currentTarget);
var uniqid = $input.attr('uniqid');
inputs = Session.get('inputs');
index = inputs.findIndex(function(x) { return x.uniqid == uniqid; });
inputs[index].value = $input.val();
Session.set('inputs', inputs);
}
});
Your templates would look something like...
<template name="test">
<button id='add-input'>
Add Input
</button>
{{#each inputs}}
{{> input}}
{{/each}}
</template>
<template name='input'>
<input name='testinput' class='test-input' type='text' uniqid="{{uniqid}}" value="{{value}}">
<button class='remove-input' uniqid="{{uniqid}}">Remove</button>
</template>
As per Ibrahim's comment below, if you want to delete the text fields, you'll need to keep track of the values in the text fields and repopulate them every time you delete an element. You can see the full work-up in action here. Note that in order to do this, I cheated and actually did use jQuery, because it was way easier to do it that way (at least for me).
A jQuery-less alternative might involve rigging up the onCreated function to store a reference to each input template instance, from which you might be able to pull the necessary information, but per this question there is no way to get all instances of a particular template through the Meteor API, which would be the easiest way to do it without jQuery.
Edit:
MeteorPad no longer exists -- The code above includes handling adding and removing a specific input using the reactive Session variable. I am now maintaining the current value of the input in the Session variable, and I use this new value property to populate the value every time the inputs are re-populated (when the Session variable updates).
You can see that constantly reading stuff off the screen and updating the array of inputs in the Session variable is quite manual and tedious -- which makes me think this is probably not the best way to be doing this.
One possible solution would be to use session variables. When the button is clicked, set the value of the session variable to what you want. In your template you can show the value of the session variable wherever you need.
Also, jquery is automatically included in meteor. There are definitely places to use jquery in meteor apps. May even be cleaner than using session variables in places. Depends on the situation.
I have an object in an array
var
sidelist = [
{
name:"MURICA",
types:[...]
}
];
I have a box that displays the object's name. Then I have a text field and a button. On button press the object's name gets set to text field value. But I don't know how to make the name in the box change accordingly.
As I understand putting the object in a session variable is not an option since I will not be able to modify properties of objects inside of it without resetting the whole session var. I tried it and failed.
html
<template name="asdf">
{{#with object}}
<div>{{name}}</div>
{{/with}}
</template>
js
Template.asdf.object = function() {
return Objects.findOne(...);
};
EDIT
I think I've got your question wrong, sorry. If you have a value in memory that you'd like to change and have the DOM updated, use dependencies:
html
<template name="asdf">
{{property}}
</template>
js
var property;
// Create new dependency object that will manage refreshing property value:
var _dep = new Deps.Dependency;
updateProperty = function(value) {
property = value;
// Whenever you change value of the property, call changed() function:
_dep.changed();
};
Template.asdf.value = function() {
// Within reactive function, call depend() to rerun the function
// each time the value is changed:
_dep.depend();
return value;
};
How about a different and in my opinion simpler solution - using a local collection for your data.
I am not sure exactly why do you keep that sort of data into an array, but if it is because you only need it on the client then you can instead create a local collection and have all the reactivity benefits without writing all that code for making the array reactive. The data stored in a local collection is never sent to the server, so no communication or storage overhead.
You'd do it like that:
Sidelist = new Meteor.Collection(null);
[EDIT] Put the above line in your client-side-only part of the code.
Notice the null parameter. This will give you a collection that is only stored on the client and is a regular Meteor reactive source. Then you go about using it in your code and html just as you would a normal collection.
Hope that helps.
I have a simple collection of messages that I want to reverse sort on time (newest on top), using comparator:
...
this.comparator = function(message) {
var time = new Date(message.get("time")).getTime();
return time;
}
...
In my view, I use fetch and add event:
messages = new MessageCollection();
messages.fetch({update: true});
messages.on("add", this.appendMessage);
...
appendMessage: function(message) {
var messageView = new MessageView({
model: message
});
this.$el.prepend(messageView.render().el);
}
Sadly, the messages are not rendered in the order I am looking for, but in the original order they were in coming from the server.
Now, after some testing I found out that when I add all the messages at once (using reset), the order is as I expected.
messages.fetch();
messages.on("reset", this.appendCollection);
...
appendCollection: function(messages) {
messages.each(function(message) {
this.appendMessage(message);
}, this);
}
Even though I can understand this process since a collection probably can only figure out how it's supposed to be sorted after all models are added, this (the on("add") configuration) used to work in Backbone 0.9.2.
Am I missing something? Did the comparator method change, or the event model in regard to add? Or am I going at it the wrong way? Thanks!
You call appendMessage method when you add a model in collection. the appendMessage is being called in the order of adding models and not the actual order in the collection.
In the "add" case, the model is inserted in the right position in the collection, as it should be by "comparator" documentation). But then you are doing
this.$el.prepend(messageView.render().el);
which will put the html from the MessageView rendering at the top of the $el (which I assume is the CollectionView container).
The best way to also keep the Html respecting the sorted order would be to re-render the collection view, or scroll the collection view children and insert the added messageView at the right place (a bit more difficult to do).
I have the following html that is bound to an object containing id and status. I want to translate status values into a specific color (hence the converter function convertStatus). I can see the converter work on the first binding, but if I change status in the binding list I do not see any UI update nor do I see convertStatus being subsequently called. My other issue is trying to bind the id property of the first span does not seem to work as expected (perhaps it is not possible to set this value via binding...)
HTML:
<span data-win-bind="id: id">person</span>
<span data-win-bind="textContent: status converter.convertStatus"></span>
Javascript (I have tried using to modify the status value):
// persons === WinJS.Binding.List
// updateStatus is a function that is called as a result of status changing in the system
function updateStatus(data) {
persons.forEach(function(value, index, array) {
if(value.id === data.id) {
value.status = data.status;
persons.notifyMutated(index);
}
}, this);
}
I have seen notifyMutated(index) work for values that are not using a converter.
Updating with github project
Public repo for sample (not-working) - this is a really basic app that has a listview with a set of default data and a function that is executed when the item is clicked. The function attempts to randomize one of the bound fields of the item and call notifyMutated(...) on the list to trigger a visual updated. Even with defining the WinJS.Binding.List({ binding: true }); I do not see updates unless I force it via notifyReload(), which produces a reload-flicker on the listview element.
To answer your two questions:
1) Why can't I set id through binding?
This is deliberately prevented. The WinJS binding system uses the ID to track the element that it's binding to (to avoid leaking DOM elements through dangling bindings). As such, it has to be able to control the id for bound templates.
2) Why isn't the converter firing more than once?
The Binding.List will tell the listview about changes in the contents of the list (items added, removed, or moved around) but it's the responsibility of the individual items to notify the listview about changes in their contents.
You need to have a data object that's bindable. There are a couple of options:
Call WinJS.Binding.as on the elements as you add them to the collection
Turn on binding mode on the Binding.List
The latter is probably easier. Basically, when you create your Binding.List, do this:
var list = new WinJS.Binding.List({binding: true});
That way the List will call binding.as on everything in the list, and things should start updating.
I've found that if I doing the following, I will see updates to the UI post-binding:
var list = new WinJS.Binding.List({binding: true});
var item = WinJS.Binding.as({
firstName: "Billy",
lastName: "Bob"
});
list.push(item);
Later in the application, you can change some values like so:
item.firstName = "Bobby";
item.lastName = "Joe";
...and you will see the changes in the UI
Here's a link on MSDN for more information:
MSDN - WinJS.Binding.as
Regarding setting the value of id.
I found that I was able to set the value of the name attribute, for a <button>.
I had been trying to set id, but that wouldn't work.
HTH
optimizeBindingReferences property
Determines whether or not binding should automatically set the ID of an element. This property should be set to true in apps that use Windows Library for JavaScript (WinJS) binding.
WinJS.Binding.optimizeBindingReferences = true;
source: http://msdn.microsoft.com/en-us/library/windows/apps/jj215606.aspx
I have a collection that can potentially contain thousands of models. I have a view that displays a table with 50 rows for each page.
Now I want to be able to cache my data so that when a user loads page 1 of the table and then clicks page 2, the data for page 1 (rows #01-50) will be cached so that when the user clicks page 1 again, backbone won't have to fetch it again.
Also, I want my collection to be able to refresh updated data from the server without performing a RESET, since RESET will delete all the models in a collection, including references of existing model that may exist in my app. Is it possible to fetch data from the server and only update or add new models if necessary by comparing the existing data and the new arriving data?
In my app, I addressed the reset question by adding a new method called fetchNew:
app.Collection = Backbone.Collection.extend({
// fetch list without overwriting existing objects (copied from fetch())
fetchNew: function(options) {
options = options || {};
var collection = this,
success = options.success;
options.success = function(resp, status, xhr) {
_(collection.parse(resp, xhr)).each(function(item) {
// added this conditional block
if (!collection.get(item.id)) {
collection.add(item, {silent:true});
}
});
if (!options.silent) {
collection.trigger('reset', collection, options);
}
if (success) success(collection, resp);
};
return (this.sync || Backbone.sync).call(this, 'read', this, options);
}
});
This is pretty much identical to the standard fetch() method, except for the conditional statement checking for item existence, and using add() by default, rather than reset. Unlike simply passing {add: true} in the options argument, it allows you to retrieve sets of models that may overlap with what you already have loaded - using {add: true} will throw an error if you try to add the same model twice.
This should solve your caching problem, assuming your collection is set up so that you can pass some kind of page parameter in options to tell the server what page of options to send back. You'll probably want to add some sort of data structure within your collection to track which pages you've loaded, to avoid doing unnecessary requests, e.g.:
app.BigCollection = app.Collection.extend({
initialize: function() {
this.loadedPages = {};
},
loadPage: function(pageNumber) {
if (!this.loadedPages[pageNumber]) {
this.fetchNew({
page: pageNumber,
success: function(collection) {
collection.loadedPages[pageNumber] = true;
}
})
}
}
});
Backbone.Collection.fetch has an option {add:true} which will add models into a collection instead of replacing the contents.
myCollection.fetch({add:true})
So, in your first scenario, the items from page2 will get added to the collection.
As far as your 2nd scenario, there's currently no built in way to do that.
According to Jeremy that's something you're supposed to do in your App, and not part of Backbone.
Backbone seems to have a number of issues when being used for collaborative apps where another user might be updating models which you have client side. I get the feeling that Jeremy seems to focus on single-user applications, and the above ticket discussion exemplifies that.
In your case, the simplest way to handle your second scenario is to iterate over your collection and call fetch() on each model. But, that's not very good for performance.
For a better way to do it, I think you're going to have to override collection._add, and go down the line dalyons did on this pull request.
I managed to get update in Backbone 0.9.9 core. Check it out as it's exactly what you need http://backbonejs.org/#Collection-update.