CanJS how to refresh a model - canjs

I have a model which represents a list of jobs which are run on the server
I want to poll the server for updates on a timer to show changes to the state of the jobs.
How do I do this?
My Control looks like this
var control = Control({
defaults: {
view: 'app/views/job-index.ejs'
}
}, {
init: function () {
this.element
.empty()
.append(can.view(this.options.view, this.options));
var options = this.options;
window.setInterval(function() {
options.result.refresh();
}, 1000);
},
});
my model, so far looks like this
var model = can.Model({
findOne: 'GET /api/jobs'
}, {
refresh: function() {
// what goes here?
}
});

One observation first:
If you are getting a collection of jobs you should probably want to use findAll instead of findOne:
findAll: 'GET /api/jobs',
findOne: 'GET /api/jobs/{id}'
I understand that result is a single record. So you can do something like:
var Model = can.Model({
findAll: 'GET /api/jobs',
findOne: 'GET /api/jobs/{id}'
}, {
refresh: function () {
var id = this.attr('id');
var self = this;
Model.findOne({id: id}, function (model) {
self.attr(model.attr());
});
}
});
Also, by convention you should name your model class Model not model.
Here is a fiddle http://jsbin.com/xarodoqo/4/edit

Related

Marionette CompositeView children.findByIndex not working as expected after collection sync

I have the following code that happens after a collection sync:
adGeneration: function() {
var child = this.children.findByIndex(this.children.length - 1);
console.log(child.model.toJSON());
eventer.trigger('generate:new:ad', child);
},
The problem I am running into, is that, after the first sync, the child element is a blank model:
First Time:
Object {id: "5-vp39kv3uiigxecdi", size: 26, price: "9.84", face: "( ⚆ _ ⚆ )"}
Every time after:
Object {length: 40, models: Array[41], _byId: Object, _listeningTo: Object, _listenId: "l14"…}
ProductsCollection
define(["backbone", "lodash", "fonts/products/model", "eventer"],
function(Backbone, _, ProductModel, eventer) {
'use strict';
var ProductsCollection = Backbone.Collection.extend({
model: ProductModel,
sort_key: 'price',
url: '/api/products',
offset: 0,
initialize: function() {
this.listenTo(eventer, 'fetch:more:products', this.loadMore, this);
},
comparator: function(item) {
return item.get(this.sort_key);
},
sortByKey: function(field) {
this.sort_key = field;
this.sort();
},
parse: function(data) {
return _.chain(data)
.filter(function(item) {
if(item.id) {
return item;
}
})
.map(function(item){
item.price = this.formatCurrency(item.price);
return item;
}.bind(this))
.value();
},
formatCurrency: function(total) {
return (total/100).toFixed(2);
},
loadMore: function() {
this.offset += 1;
this.fetch({
data: {
limit: 20,
skip: this.offset
},
remove: false,
success: function(collection) {
this.add(collection);
}.bind(this)
});
}
});
return ProductsCollection;
});
LayoutView that contains the View for the productions collection. The collection is fetched onShow of the layoutview
define(["marionette", "lodash", "text!fonts/template.html",
"fonts/controls/view", "fonts/products/view", "fonts/products/collection", "eventer"],
function(Marionette, _, templateHTML, ControlsView, ProductsView,
ProductsCollection, eventer) {
'use strict';
var FontsView = Marionette.LayoutView.extend({
regions: {
controls: '#controls',
products: '#products-list'
},
template: _.template(templateHTML),
initialize: function() {
this._controlsView = new ControlsView();
this._productsView = new ProductsView({
collection: new ProductsCollection({
reorderOnSort: false,
sort: false
})
});
this.listenTo(this._productsView.collection, 'sync', this.loading, this);
this.listenTo(eventer, 'fetch:more:products', this.loading, this);
this.listenTo(eventer, 'products:end', this.productsEnd, this);
},
onRender: function() {
this.getRegion('controls').show(this._controlsView);
this.getRegion('products').show(this._productsView);
this.loading();
},
onShow: function() {
this._productsView.collection.fetch({
data: {
limit: 20
}
})
},
productsEnd: function() {
this.loading();
this.$el.find('#loading').html("~ end of catalogue ~")
},
loading: function() {
var toggle = this.$el.find('#loading').is(':hidden');
this.$el.find('#loading').toggle(toggle);
}
});
return FontsView;
});
AdsView:
define(["marionette", "lodash", "text!ads/template.html", "eventer"],
function(Marionette, _, templateHTML, eventer) {
'use strict';
var AdsView = Marionette.ItemView.extend({
template: _.template(templateHTML),
ui: {
ad: '.ad'
},
initialize: function() {
this.listenTo(eventer, 'generate:new:ad', this.generateNewAd, this);
},
onShow: function() {
// Set add image onShow
this.ui.ad.prop('src', '/ad/' + this.randomNumber());
},
generateNewAd: function(childView) {
var newAd = this.ui.ad.clone(),
element = childView.$el,
elementId = childView.model.get("id");
newAd.prop('src', '/ad/' + this.randomNumber());
$("#" + elementId).after(newAd);
},
randomNumber: function() {
return Math.floor(Math.random()*1000);
},
setUpAd: function() {
this.ui.ad.prop('src', '/ad/' + this.randomNumber());
}
});
return AdsView;
});
I think your problem is in the ProductsCollection.loadMore method. There, in the success callback to your fetch you do,
function(collection) { this.add(collection); }
What's happening behind the scenes is that before your success callback is invoked, Backbone will first run Collection.set() on your data. By default, inside set your data will be parsed into a array of models returned by ProductsCollection.parse and if any new models are found they will be add'ed to your existing collection (note that unless you pass { remove: false } to your fetch options, models in your collection which are not in your last fetch will be removed. See Collection.set)
So, what happens when you do the fetch in loadMore, which is called after the first fetch, Backbone will first add all the models from the server (that are returned from ProductsCollection.parse) and then invoke the success callback of your fetch, which, essentially does one last add. And what it's add'ing is the ProductsCollection instance. Not collection.models, an array of models, rather a Backbone object that has a property models that holds a raw array of models. Hence, the strange output:
Object {length: 40, models: Array[41], _byId: Object, _listeningTo: Object,
_listenId: "l14"…}
Simply remove that success callback (which is unnecessary) and the last child view of your ProductsView should be the view rendered from the last model returned.

Unit-testing remote methods of a strongloop loopback.io model

I am trying to write unittests for a loopback model using jasmine. My model has the usual CRUD endpoints but I have defined a custom '/products/:id/upload' endpoint which expects a form with files.
My model looks like
'use strict';
var loopback = require('loopback');
var ProductSchema = {
location: {
type: String,
required: true
},
version: {
type: String,
required: true
},
id: { type: Number, id: 1, generated: true }
};
var opts = {
strict: true
};
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
var Product = dataSource.createModel('Product', ProductSchema, opts);
Product.beforeRemote('upload', function(ctx){
var uploader = function(req, res){
// parse a multipart form
res({
result:'success'
});
};
function createProduct(uploaderResult){
// create a product out of the uploaded file
ctx.res.send({
result: uploaderResult.result
});
}
uploader.upload(ctx.req, createProduct);
});
Product.upload = function () {
// empty function - all the logic takes place inside before remote
};
loopback.remoteMethod(
Product.upload,
{
accepts : [{arg: 'uploadedFiles', http: function(ctx){
return function() {
return { files : ctx.req.body.uploadedFiles, context : ctx };
};
}},
{arg: 'id', type: 'string'}],
returns : {arg: 'upload_result', type: String},
http: {path:'/:id/upload', verb: 'post'}
}
);
module.exports = Product;
My end goal is to test the logic of the "createProduct".
My test looks like
'use strict';
describe('Product Model', function(){
var app = require('../../app');
var loopback = require('loopback');
var ProductModel;
beforeEach(function(){
app = loopback();
app.boot(__dirname+'/../../'); // contains a 'models' folder
ProductModel = loopback.getModel('Product');
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
ProductModel.attachTo(dataSource);
});
it('should load file ', function(){
console.log(ProductModel.beforeRemote.toString());
console.log(ProductModel);
ProductModel.upload();
});
});
By calling ProductModel.upload(); I was hoping to trigger the before remote hook which would exercise the the createProduct. I could test "createProduct" in isolation but then I would omit the fact that createProduct ends up being called as a result of upload.
To be perfectly clear, the core question is:
How do I exercise remote method hooks inside unittests ?
It was suggested to use supertest as an http server. Below there is a code snippet illustrating how to do it in jasmine
describe('My product suite', function(){
var request = require('supertest');
var app;
beforeEach(function(){
app = loopback();
// don't forget to add REST to the app
app.use(app.rest());
});
it('should load file', function() {
request(app).post('/products/id-of-existing-product/upload')
.attach('file', 'path/to/local/file/to/upload.png')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
// res is the HTTP response
// you can assert on res.body, etc.
});
});
});

Ember: Fetching data for objects that depend on each other

I'm trying to build a front-end for a metrics tool with Ember. The code that I've written so far has been very much influenced by Eviltrout's emberreddit application
https://github.com/eviltrout/emberreddit
The goal is to have two classes that depend on each other: metrics and filters.
1) Once the application initializes, the filters, which are instances of the Filter-class, are loaded from the server. Once the filters have loaded, they are displayed as checkboxes on the screen. After that, the metrics objects should take the filters as parameters and query the server for data.
2) Once the user changes the checkboxes and thus updates the filter objects, the application should take the filters as parameters again and fetch new metrics data from the server.
My problem is that I don't know how to handle the dependencies between these two sets of objects with asynchronous ajax calls. At it's current state, my application doesn't finish loading the filters when it already starts loading the metrics. Therefore, the filters don't get passed as parameters for the metrics ajax-call.
My question is: What's the best way to do this ember? There surely has to be a way to handle the order of ajax calls. My intuition is that manually adding observers isn't the way to go.
Here are the models of my application:
//FILTER MODELS
var defaultFilters = ['dates', 'devices'];
//set Filter class. The Filter object will be multiplied for each filter.
App.Filter = Ember.Object.extend({
//capitalize first letter to get title
filterTitle: function() {
return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
}.property('id'),
//set attribute to see if filter has loaded
loadedFilter: false,
//create method to load filter values from server
loadValues: function() {
var filter = this;
return Ember.Deferred.promise(function (p) {
if (filter.get('loadedFilter')) {
p.resolve(filter.get('values'));
} else {
p.resolve($.getJSON("http://127.0.0.1:13373/options/" + filter.get('id')).then(function(response) {
var values = Ember.A();
response[filter.get('id')].forEach(function(value) {
values.push(value);
});
filter.setProperties({values: values, loadedFilter: true});
return values;
}))
}})}
}
);
//reopen class to create "all" method which returns all instances of Filter class
App.Filter.reopenClass({
all: function() {
if (this._all) {return this._all; }
var all = Ember.A();
defaultFilters.forEach(function(id) {
all.pushObject(App.Filter.create({id: id}));
});
this._all = all;
return all;
}});
//Create a Filters array to store all the filters.
App.Filters = App.Filter.all();
//METRIC MODELS
App.Metric = Ember.Object.extend({
metricTitle: function() {
return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
}.property('id'),
loadedMetric: false,
filtersBinding: 'App.Filters',
loadValues: function() {
var metric = this;
var filters = metric.get('filters');
if (filters.get('loadedFilters'))
console.log('loading metrics');
return Ember.Deferred.promise(function (p) {
if (metric.get('loadedMetric')) {
p.resolve(metric.get('values'));
} else {
p.resolve(
console.log('sending ajax'),
$.ajax({
url: "http://127.0.0.1:13373/" + metric.get('id') + "/",
data: JSON.stringify(metric.get('filters')),
}).then(function(response) {
var values = Ember.A();
response[metric.get('id')].forEach(function(value) {
values.push(value);
});
metric.setProperties({"values": values, "loadedMetric": true});
return values;
}))
}})}
});
App.Metric.reopenClass({
findByView: function(searchView) {
if (this._metrics) {return this._metrics; }
var metrics = Ember.A();
defaultMetricsSettings.forEach(function(metric) {
if (metric.view == searchView)
metrics.pushObject(App.Metric.create({id: metric.id},{view: metric.view}, {calculation: metric.calculation}, {format: metric.format}, {width: metric.width}));
});
this._metrics = metrics;
return metrics;
}
});
And here are the routes:
App.ApplicationRoute = Ember.Route.extend({
//set application routes model to all filters
model: function() {
return App.Filter.all();
},
//after filter has loaded, let's load its values
afterModel: function(model) {
return model.forEach(function(item) {
item.loadValues();
});
},
//create a controller called ApplicationController and pass the filter as its model
setupController: function(controller, filter) {
controller.set('model', filter);
}
});
App.DashboardRoute = Ember.Route.extend({
model: function() {
return App.Metric.findByView('Dashboard');
},
afterModel: function(model) {
return model.forEach(function(item) {
item.loadValues();
});
},
setupController: function(controller, metric) {
controller.set('model', metric);
}
});
Controllers:
App.ApplicationController = Ember.ArrayController.extend({
//ApplicationController controls all the filters. Let's create a controller to handle each instance of a filter
itemController: 'filter'
});
App.FilterController = Ember.ObjectController.extend({
//this sets the titleId property that is used only for binding html attributes in template. Stupid way to do this.
titleId: function() {
return "#" + this.get('filterTitle');}.property('filterTitle')
});
Your afterModel hook could do this in a sequence of dependent promises. The current implementation is returning immediately, instead you chain the promise and finally return the last promise as the result of the hook. The router will wait for the whole set of calls to complete before continuing to setupController.
afterModel: function(model) {
var promise;
model.forEach(function(item)) {
if (promise) {
promise = promise.then(function() {
item.loadValues();
});
} else {
promise = item.loadValues();
}
}
return promise;
}
I'm not sure how many of the calls you have, but you may want to batch some of these together to reduce the number of HTTP requests.

backbone collection add does not trigger model validate

I am rather new to backbone and wanted to test a simple script that handles a to do list. Here is the code i used so far:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {}
};
window.template = function(id) {
return _.template($('#' + id).html());
}
App.Models.Task = Backbone.Model.extend({
validate: function(attributes) {
if ( !$.trim(attributes.title) ) {
return 'Invalid title';
}
}
});
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task
});
App.Views.Task = Backbone.View.extend({
tagName: 'li',
template: template('taskTemplate'),
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .edit': 'editTask',
'click .delete': 'destroy'
},
destroy: function() {
if (confirm('Are you sure?')) {
this.model.destroy();
}
},
remove: function() {
this.$el.remove();
},
editTask: function() {
var newTaskTitle = prompt('New title:', this.model.get('title'));
this.model.set('title', newTaskTitle, {validate: true});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
App.Views.AddTask = Backbone.View.extend({
el: 'form#addTask',
initialize: function() {
},
events: {
'submit': 'submit'
},
submit: function(event) {
event.preventDefault();
var newTaskTitle = $(event.currentTarget).find('input[type=text]').val();
var task = new App.Models.Task({ title: newTaskTitle });
this.collection.add(task, {add: true, merge: false, remove: false});
}
});
App.Views.Tasks = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('add', this.addOne, this);
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.Task({ model: task });
this.$el.append(taskView.render().el);
}
});
var tasks = new App.Collections.Tasks([
{
title: 'Go to store',
priority: 4
},
{
title: 'Go to mall',
priority: 3
},
{
title: 'Get to work',
priority: 5
}
]);
var addTaskView = new App.Views.AddTask({ collection: tasks });
var tasksView = new App.Views.Tasks({ collection: tasks });
$('div.tasks').append(tasksView.render().el);
})();
So the model validation works fine ... the only pb is that collection.add does not validate the newly added model .... is the a way to force the validation?
Thanks,
Rares
From the fine manual:
validate model.validate(attributes, options)
[...] By default validate is called before save, but can also be
called before set if {validate:true} is passed.
Collection#add does not call save nor does it call set with the validate: true option. If you want to validate during add, say so:
collection.add(models, { validate: true });
That will get validate:true all that way down to Model#set.
A quick look at a simplified example may be helpful:
var M = Backbone.Model.extend({
set: function() {
console.log('setting...');
Backbone.Model.prototype.set.apply(this, arguments);
},
validate: function() {
console.log('validating...');
return 'Never!';
}
});
var C = Backbone.Collection.extend({
model: M
});
var c = new C;
c.on('add', function() {
console.log('Added: ', arguments);
});
c.on('invalid', function() {
console.log('Error: ', arguments);
});
Now if we do this (http://jsfiddle.net/ambiguous/7NqPg/):
c.add(
{ where: 'is', pancakes: 'house?' },
{ validate: true }
);
You'll see that set is called with validate: true, validate will be called, and you'll get an error. But if you say this (http://jsfiddle.net/ambiguous/7b2mn/):
c.add(
{ where: 'is', pancakes: 'house?' },
{add: true, merge: false, remove: false} // Your options
);
You'll see that set is called without validate: true, validate will not be called, and the model will be added to the collection.
The above behavior is quite strongly implied but not explicitly specified so you may not want to trust it. Model#initialize does say:
you can pass in the initial values of the attributes, which will be set on the model.
and set does explicitly mention the validate option. However, there is no guarantee that Collection#add will send options to the model constructor or set or that the model's constructor will send the options to set. So if you want to be really paranoid and future proof, you could add a quick check for this "options get all the way down to set" behavior to your test suite; then, if it changes you'll know about it and you can fix it.
if you pass options to your collection add method, the validation method will not be called and as your arguments in this case are all set to the default value, there is not need to pass them
this.collection.add(task);
you may want to take a look at this question.
Prevent Backbone.js model from validating when first added to collection

Trigger sort method from an event in Backbone.js

I have a Backbone.js project which uses a comparator function defined in the collection. It sorts items when the page is refreshed, but I am trying to get it to sort when a button is clicked instead of on page refresh. Here is my code:
var Thing = Backbone.Model.extend({
defaults: {
title: 'blank',
rank: ''
}
});
var ThingView = Backbone.View.extend({
className: 'thingClass',
template: _.template('<b><button id="remove">X</button> <b><button id="edit">Edit</button> <%= title %> Rank:<%= rank %></b>'),
editTemplate: _.template('<input class="name" value="<%= name %>" /><button id="save">Save</button>'),
events: {
"click #remove": "deleteItem",
"click #edit": "editItem",
"click #save": "saveItem",
},
deleteItem: function () {
console.log('deleted');
this.model.destroy();
this.remove();
},
editItem: function () {
console.log('editing');
this.$el.html(this.editTemplate(this.model.toJSON()));
},
saveItem: function () {
console.log('saved');
editTitle = $('input.name').val();
console.log(editTitle);
this.model.save({
title: editTitle
});
this.$el.html(this.template(this.model.toJSON()));
},
render: function () {
var attributes = this.model.toJSON();
//console.log (attributes);
this.$el.append(this.template(attributes));
return this;
}
});
var ThingsList = Backbone.Collection.extend({
model: Thing,
localStorage: new Store("store-name"),
comparator: function(thing) {
return thing.get('rank');
},
});
var thingsList = new ThingsList;
var ThingsListView = Backbone.View.extend({
el: $('body'),
events: {
'click #add': 'insertItem',
'click #sort': 'sortItems',
},
initialize: function () {
thingsList.fetch();
thingsList.toJSON();
this.render();
this.collection.on("add", this.renderThing, this);
this.collection.on("reset", this.clearRender, this);
},
insertItem: function (e) {
newTitle = $('#new-item').val();
newRank = $('#rank').val();
newThing = new Thing({
title: newTitle,
rank: newRank
});
this.collection.add(newThing);
newThing.save();
console.log(this.collection.length);
},
sortItems: function (e) {
console.log('clicked sort button');
this.collection.sort();
this.$el.detach('.item');
},
render: function () {
_.each(this.collection.models, function (items) {
this.renderThing(items);
}, this);
},
renderThing: function (items) {
var thingView = new ThingView({
model: items
});
this.$el.append(thingView.render().el);
},
clearRender: function () {
console.log('clear render called')
_.each(this.collection.models, function (items) {
//this.remove();
this.$el.remove(".thingClass")
this.renderThing(items);
}, this);
},
test: function (items) {
console.log('test worked');
},
});
var thingsListView = new ThingsListView({
collection: thingsList
});
Are you sure your collection isn't resorting itself? keep in mind that the order of the models in the collection won't change the order of how they appear on the page if they are already rendered.
I'm guessing that what you are trying to do is resort the items that have already been rendered, to do so you would need re-render your collection. If you are going to do so I would recommend that you cache your views and on a sort detach the associated element from the DOM and reattach them in the correct order.
As an example
var ThingsListView = Backbone.View.extend({
_views: {},
initialize: function () {
this.collection.bind('add', this.add, this);
this.collection.bind('reset', this.render, this); //sort triggers a reset
},
add: function (thing) {
var view = new ThingView({model: thing});
this._views[thing.cid] = view; //use client id of model as key for the views cache
this.$el.append(view.render().el);
},
render: function() {
$('li, this.$el).detach(); //detach so that bound events aren't lost
_.each(this.collection.models, function(thing) {
this.$el.append(this.views[thing.cid].el); //get view from cache
},this);
},
sort: function() {
this.collection.sort();
}
}
})
(a couple of differences from my example code and yours I'm assuming here that the collection view has a 'el' referring to a container 'ul', I also don't show how your triggering the sort (basically something like thingListView.sort();)
Edit: It might not be so obvious from the example code I posted, so I should have mentioned to begin with what #Deeptechtons said that when you sort a collection it triggers a reset event
Edit2: If your not interested in caching your views, then the easiest way to remove your current views would probablly be to add a class to the rendered div
var ThingView = Backbone.View.extend({
className: 'thingClass',
//rest of your thingViewCode
Then in your clearRender method just add $(".thingClass", this.$el).remove(); to the beginning of the method.

Resources