I'm trying to wrap my mind around Backbone (as my recent flurry of questions indicate...). In particular I'm working through this project:
https://github.com/ccoenraets/nodecellar
http://nodecellar.coenraets.org/#
I want to conceptually understand what happens when I click the "Save" button on a new Wine for example this one:
http://nodecellar.coenraets.org/#wines/506df6b6849a990200000001
I'm thinking it goes something like this:
1) The Backbone winedetails view catches the save button click as an event and launches the "Before Save" method. See below from /public/js/views/winedetails.js.
beforeSave: function () {
var self = this;
var check = this.model.validateAll();
if (check.isValid === false) {
utils.displayValidationErrors(check.messages);
return false;
}
this.saveWine();
return false;
},
saveWine: function () {
var self = this;
console.log('before save');
this.model.save(null, {
success: function (model) {
self.render();
app.navigate('wines/' + model.id, false);
utils.showAlert('Success!', 'Wine saved successfully', 'alert-success');
},
error: function () {
utils.showAlert('Error', 'An error occurred while trying to delete this item', 'alert-error');
}
});
},
In that Save method (the 2nd method) there is a call to this.model.save. SOMEHOW that model save method MUST be making a PUT request to the '/wines' URL as evidenced in the server.js file (This is for a node.js server):
app.get('/wines', wine.findAll);
app.get('/wines/:id', wine.findById);
app.post('/wines', wine.addWine);
app.put('/wines/:id', wine.updateWine);
app.delete('/wines/:id', wine.deleteWine);
From there obviously it runs the addWine method which is defined in the routes/wines.js. What I don't understand is how the MODEL understands which URL to send the request to. I can't find anywhere that links the model.save method with the correct REST API. Does my question make sense?
Wait I might have answered my own question. It must be this line in: /public/js/models/models.js
urlRoot: "/wines"
And then Backbone knows if you are doing an "New" model it must send a POST request. If you are doing an update it must append the :id to the URL and send a PUT request, etc. Is that how it works?
Here is the documentation for the model urlRoot : http://backbonejs.org/#Model-urlRoot
If you have specified the urlRoot on the model, it will use that. If the model is part of a collection, it will reference the url property on the collection.
When saving, Backbone will use PUT for an update and POST for a create. It determines which is should use based on the result of the isNew function. This checks whether the model has an id property.
Related
I am having a bit of trouble with getting values from Protractor testing and being able to reuse those values.
I have an app that creates new records from a form and then displays them back to the user. On a successful addition, the user is presented with a success alert, displaying the ID of the newly created record. "You have successfully added an entry with the ID {{newEntry.id}}".
I have a suite of tests asserting that all the fields are correctly validated etc, which all work correctly. I now want to test the Update side of things by taking the newly created record and testing if a new set of values updates correctly. Therefore I want to take that ID of the newly created record and reuse it.
I have created the variable ID at the top of my suite,
var id;
I then run all the validation tests on the form and submit a correct submission. I then check if the success message is shown and that, in this instance, the ID = 2.
describe('Add users', function() {
var endpoint = "users";
var id;
correctSubmission(endpoint, id);
function correctSubmission(endpoint, id) {
describe('Testing correct submission', function() {
it('should navigate back to the list page', function() {
expect(browser.getCurrentUrl()).toBe("list/" + endpoint);
});
it('should display a success message', function() {
expect(element(by.css('.alert-success')).isPresent()).toBeTruthy();
});
it('should get the record ID from the success message', function() {
expect(element(by.css('.add-message')).evaluate('newEntry.id')).toEqual(2);
id = element(by.css('.add-message')).evaluate('newEntry.id');
return id;
});
});
};
});
I need to basically get that ID that equals 2 and return it back to the Global ID so that I can use it across other tests. Obviously that ID is currently an unresolved promise, and I have tried to use:
protractor.promise.all(id).then(function (result) {
console.log("ID is: " + result);
});
But this only logs the string.
I am a bit lost with what to do next as I have tried all sorts, but to no avail and I am pushed for time on this project.
Many thanks if you can help this Protractor n00b.
did you try using a protractor params config attribute?
exports.config = {
params: {
myid: 'somevaluehere'
}
};
Then u can access it by
browser.params.myid
I've built an app that is form-based. I want to enable users to partially fill out a form, and then come back to it at a later date if they can't finish it at the present. I've used iron router to create a unique URL for each form instance, so they can come back to the link. My problem is that Meteor doesn't automatically save the values in the inputs, and the form comes up blank when it is revisited/refreshes. I tried the below solution to store the data in a temporary document in a separate Mongo collection called "NewScreen", and then reference that document every time the template is (re)rendered to auto fill the form. However, I keep getting an error that the element I'm trying to reference is "undefined". The weird thing is that sometimes it works, sometimes it doesn't. I've tried setting a recursive setTimeout function, but on the times it fails, that doesn't work either. Any insight would be greatly appreciated. Or, if I'm going about this all wrong, feel free to suggest a different approach:
Screens = new Meteor.Collection('screens') //where data will ultimately be stored
Forms = new Meteor.Collection('forms') //Meteor pulls form questions from here
NewScreen = new Meteor.Collection('newscreen') //temporary storage collection
Roles = new Meteor.Collection('roles'); //displays list of metadata about screens in a dashboard
//dynamic routing for unique instance of blank form
Router.route('/forms/:_id', {
name: 'BlankForm',
data: function(){
return NewScreen.findOne({_id: this.params._id});
}
});
//onRendered function to pull data from NewScreen collection (this is where I get the error)
Template.BlankForm.onRendered(function(){
var new_screen = NewScreen.findOne({_id: window.location.href.split('/')[window.location.href.split('/').length-1]})
function do_work(){
if(typeof new_screen === 'undefined'){
console.log('waiting...');
Meteor.setTimeout(do_work, 100);
}else{
$('input')[0].value = new_screen.first;
for(i=0;i<new_screen.answers.length;i++){
$('textarea')[i].value = new_screen.answers[i];
}
}
}
do_work();
});
//onChange event that updates the NewScreen document when user updates value of input in the form
'change [id="on-change"]': function(e, tmpl){
var screen_data = [];
var name = $('input')[0].value;
for(i=0; i<$('textarea').length;i++){
screen_data.push($('textarea')[i].value);
}
Session.set("updateNewScreen", this._id);
NewScreen.update(
Session.get("updateNewScreen"),
{$set:
{
answers: screen_data,
first: name
}
});
console.log(screen_data);
}
If you get undefined that could mean findOne() did not find the newscreen with the Id that was passed in from the url. To investigate this, add an extra line like console.log(window.location.href.split('/')[window.location.href.split('/').length-1], JSON.stringify(new_screen));
This will give you both the Id from the url and the new_screen that was found.
I would recommend using Router.current().location.get().path instead of window.location.href since you use IR.
And if you're looking for two way binding in the client, have a look at Viewmodel for Meteor.
I've got a page for which I poll for notifications. If there are any notifications I populate a Backbone.js view with the notifications. I currently poll for notifications every 2 seconds and simply repopulate the view every two seconds.
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
$.when(ticketNotificationCollection.fetch()).done(function() {
var notificationView = new NotificationsView({collection: ticketNotificationCollection});
$('#ticket-notifications-area').html(notificationView.render().el);
});
}
window.notificationsIntervalId = setInterval(function() {updateNotificationView(ticketId);}, 2000);
I now want to only populate the view if the Backbone fetched collection has changed, but I have no idea how I could do that?
Could anybody give me a tip on how I could only populate the view on collection change? All tips are welcome!
[EDIT]
I now changed the function to this:
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
var notificationsView = new NotificationsView();
notificationsView.listenTo(ticketNotificationCollection, 'change', notificationsView.render);
notificationsView.listenTo(ticketNotificationCollection, 'add', notificationsView.render);
ticketNotificationCollection.fetch();
}
and I changed the NotificationsView to this:
var NotificationsView = Backbone.View.extend({
addNotificationView: function(notification) {
var singleNotificationView = new SingleNotificationView({model: notification});
this.$el.append(singleNotificationView.render().el);
},
render: function() {
console.log('ITS BEING CALLED!!');
this.collection.each(this.addNotificationView, this);
$('#ticket-notifications-area').html(this);
}
});
I now get the "ITS BEING CALLED!!" in the console every two seconds, but also an error:
"Uncaught TypeError: Cannot read property 'each' of undefined.". I get why this error occurs; its because the collection is never actually inserted into the view. I don't get however, how I can solve this.. Any more tips?
I am assuming that you are getting models in the collection in response to your request as JSON.
Method 1:
You can simply set the collection to the JSON. and bind the collection to change event. In case of any change in the collection, callback function will be triggered. In the callback function you can render your view.
Note: change event on collection will be triggered only if there is any change in the models it contain.
e.g.
someCollection.on('change', function(){
someView.render()
});
Method 2:
You can use Backbone's listenTo method, to render the view on change of collection.
someView.listenTo(someCollection, 'change', someView.render);
Hope it helps...
I have just started trying knockout.js. The ko.mapping offers a nifty way to get and map data from server. However I am unable to get the mapping to work.
I have a simple model:
//var helloWorldModel;
var helloWorldModel = {
name: ko.observable('Default Name'),
message: ko.observable('Hello World Default')
};
$(document).ready(function() {
ko.applyBindings(helloWorldModel);
//a button on the form when clicked calls a server class
//to get json output
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
alert(JSON.stringify(serverData));
}
The helloWorldModel has only 2 attributes - exactly the same thing I return from the server. The alert in mapServerData shows -
{"name":"chicken","message":"JSON hello world"}
I have looked up other posts regarding similar problem, but none of them seemed to be solve this issue. Maybe I am missing something very basic - wondering if anyone can point it out.
Also note if I do not declare the model upfront and use
helloWorldModel = ko.mapping.fromJS(serverData);
it is mapping the data to my form correctly.
From Richard's reply and then a little more investigation into this I think that the way I was initializing the model is incorrect. I guess that one cannot use an existing view model and then expect it to work with mapper plugin. So instead you initialize view model with raw JSON data using the ko.mapping.fromJS:
var helloWorldModel;
$(document).ready(function() {
var defaultData = {
name: 'Default Name',
message: 'Hello World Default'
};
helloWorldModel = ko.mapping.fromJS(defaultData);
ko.applyBindings(helloWorldModel);
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
alert(JSON.stringify(serverData));
ko.mapping.fromJS(serverData, helloWorldModel);
}
This code works and provides the expected behavior
You can't just overwrite your model by reassigning it this way.
When you do:
ko.applyBindings(helloWorldModel);
You are saying "bind the model helloWorldModel to the page". Knockout then goes through and hooks up the observables in that model and binds them with the page.
Now when you overwrite your form model here:
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
It is overwriting your model object with a brand new object with entirely new observables in it.
To fix it you need to change this line to just:
ko.mapping.fromJS(serverData, helloWorldModel);
This takes care of the properties inside the model and reassigns them for you, without overwriting your model.
I may be completely missing something here, but I have the following:
a Model which encapsulates 'all' the data (all JSON loaded from one URL)
the model has one (or more) Collections which it is instantiating with the data it got on construction
some code which I want to run on the Collection when the data is initialized and loaded
My question is about the composed Collection. I could do this outside the scope of the Collection, but I'd rather encapsulate it (otherwise what's the point of making it a 'class' with an initializer etc).
I thought I could put that code in the initialize() function, but that runs before the model has been populated, so I don't have access to the models that comprise the collection (this.models is empty).
Then I thought I could bind to an event, but no events are triggered after initialization. They would be if I loaded the Collection with a fetch from its own endpoint, but I'm not doing that, I'm initializing the collection from pre-existing data.
My question: How to get initialize code to run on the Collection immediately after it is initialized with data (i.e. this.models isn't empty).
Is it possible to do this without having to get 'external' code involved?
Okay here is the demo code, perhaps this will explain things better.
var Everything = Backbone.Model.extend({
url: "/static/data/mydata.json",
parse: function(data)
{
this.set("things", new Things(data.things, {controller: this}));
}
});
var Thing = Backbone.Model.extend({
});
var Things = Backbone.Collection.extend({
model: Thing,
initialize: function(data, options)
{
// HERE I want access to this.models.
// Unfortunately it has not yet been populated.
console.log("initialize");
console.log(this.models);
// result: []
// And this event never gets triggered either!
this.on("all", function(eventType)
{
console.log("Some kind of event happend!", eventType);
});
}
});
var everything = new Everything();
everything.fetch();
// Some manual poking to prove that the demo code above works:
// Run after everything has happened, to prove collection does get created with data
setTimeout(function(){console.log("outside data", everything.get("things").models);}, 1000);
// This has the expected result, prints a load of models.
// Prove that the event hander works.
setTimeout(function(){console.log("outside trigger", everything.get("things").trigger("change"));}, 1000);
// This triggers the event callback.
Unfortunately for you the collection gets set with data only after it was properly initialized first and models are reset using silent: true flag which means the event won't trigger.
If you really wanted to use it you can cheat it a bit by delaying execution of whatever you want to do to next browser event loop using setTimeout(..., 0) or the underscore defer method.
initialize: function(data, options) {
_.defer(_.bind(this.doSomething, this));
},
doSomething: function() {
// now the models are going to be available
}
Digging this an old question. I had a similar problem, and got some help to create this solution:
By extending the set function we can know when the collection's data has been converted to real models. (Set gets called from .add and .reset, which means it is called during the core function instantiating the Collection class AND from fetch, regardless of reset or set in the fetch options. A dive into the backbone annotated source and following the function flow helped here)
This way we can have control over when / how we get notified without hacking the execution flow.
var MyCollection = Backbone.Collection.extend({
url: "http://private-a2993-test958.apiary-mock.com/notes",
initialize: function () {
this.listenToOnce(this, 'set', this.onInitialized)
},
onInitialized:function(){
console.log("collection models have been initialized:",this.models )
},
set: function(models,options){
Backbone.Collection.prototype.set.call(this, models, options);
this.trigger("set");
}
})
//Works with Fetch!
var fetchCollection= new MyCollection()
fetchCollection.fetch();
//Works with initializing data
var colData = new MyCollection([
{id:5, name:'five'},
{id:6, name:'six'},
{id:7, name:'seven'},
{id:8, name:'eight'}
])
//doesn't trigger the initialized function
colData.add(new Backbone.Model({id:9,name:'nine'};
Note: If we dont use .listenToOnce, then we will also get onInitialized called every time a model is added to or changed in the collection as well.