i was wondering how to call lastObject, nextObject on my Model/Store? I always get a 'Has no method'-Error.
Both methods are defined here:
http://emberjs.com/api/classes/Ember.Array.html
App.Photo = DS.Model.extend
url: DS.attr('string'),
displayName: DS.attr('string')
App.Store = DS.Store.extend
revision: 12,
adapter: 'DS.FixtureAdapter'
App.PhotoController = Ember.ObjectController.extend
init: ->
console.info "last", App.Photo.lastObject
console.info "next", App.Photo.nextObject(#get('content'))
Update/Working Solution
console.info "last", App.Photo.find().get('lastObject')
App.Photo is not an array. It's a class.
App.Photo.find() will return an array-like object, a record array of all photos in the store, which will have a lastObject property, but you won't be able to call it like a function. Furthermore, #nextObject is overridden on arrays to be equivalent to #objectAt, so that won't work either (it doesn't work 'like that' either way, actually).
Related
I am using backbone and a workflow.js extension with to set up flow of my application. I have following backbone model
var InvoiceModel = Backbone.Model.extend({
workflow: {
initial: 'draft',
events: [
{ name: 'issue', from: 'draft', to: 'issued' },
{ name: 'payout', from: 'issued', to: 'paid' },
{ name: 'cancel', from: 'draft', to: 'canceled' },
{ name: 'cancel', from: 'issued', to: 'canceled' }
]
},
initialize: function () {
_.extend(this, new Backbone.Workflow(this, { attrName: 'status' }));
}
});
I am using above model like below
var invoiceModel = new InvoiceModel();
console.log(invoiceModel.get('status'));
invoiceModel.triggerEvent('issue'); // Uncaught TypeError: undefined is not a function
Somehow when I use triggerEvent() function is returns undefined is not a function. Why and how I can fix this error?
UPDATE
Here is the FIDDLE
The method triggerEvent() hides in the prototype of the Workflow object.
_.extend() only copies object's own properties without properties from the prototype.
Your example will work with:
initialize: function () {
_.extend(this, (new Backbone.Workflow(this, {
attrName: 'status'
})).__proto__, new Backbone.Workflow(this, {
attrName: 'status'
}));
}
As you can see, I explicitly added a prototype to the extend chain.
Know that it's a very inelegant solution. Though I didn't find a better one yet, I hope it will explain your problem.
This also works:
var Workflow = new Backbone.Workflow(this, {
attrName: 'status'
});
_.extend(this, Workflow);
$.extend(this, Workflow);
Here Underscore copies Workflows own properties (model) and jQuery deep-copies the properties from the prototype.
triggerEvent is not a Backbone.Model method. You use trigger to emit an event. If that triggerEvent is coming from your Workflow library (I couldn't find any link/info after a quick googling) then you're probably setting up your classes wrong.
I don't think your initialize method is quite right, but without seeing the Workflow docs I can't tell you more.
However, the general pattern to augment Backbone components is to extend the Model definition's prototype with another class methods, not extending the class instance (this in your initialize method) with a new instance.
I'm pretty sure that's where the bug lies. ;)
I'm having a problem with my sibling hasMany relationships disappearing. Working with Ember data canary.
I have the following data model:
import DS from 'ember-data';
export default DS.Model.extend({
// More here, discarded for brevity...
app: DS.hasMany('app', { async: true }),
paymentMethod: DS.hasMany('paymentMethod', { async: true })
});
When user is updated after deleting a paymentMethod in the following way:
var paymentMethod = this.get('content'),
currentUser = this.session.get('currentUser.content');
currentUser.get('paymentMethod').then(function ( paymentMethods ) {
paymentMethods.removeObject(paymentMethod.get('id'));
paymentMethod.destroyRecord();
currentUser.save();
}, handleError);
or saving in the following way:
var paymentMethod = self.store.createRecord('payment-method', paymentMethodData);
paymentMethod.save().then(function ( PaymentMethod ) {
currentUser.get('paymentMethod').addObject(PaymentMethod);
currentUser.save().then(function ( /* record */ ) {...
The apps array is set to an empty []. It happens the opposite way as well, deleteing or adding an app with a paymentMethod will unset the paymentMethod array.
I have the following serializer in place, but it appears as the relationship is set as an empty array before the record gets to the serializer:
var json = {
_id: user.get('id'),
name: {
first: user.get('firstName'),
last: user.get('lastName'),
company: user.get('companyName')
},
login: {
email: user.get('email'),
password: user.get('password')
},
app: user.get('app').mapProperty('id'),
paymentMethod: user.get('paymentMethod').mapProperty('id'),
time_stamp: user.get('time_stamp')
};
return json;
Sorry for the overload. Hope you can help.
You are naming your hasMany associations in singular, which isn't really following the convention. That being said, you have no 'apps' array. I don't think that should cause you any problems, I am just pointing out because you maybe searching for the wrong thing.
I suppose your backend somehow restricts you to this payload?
I'm writing Qunit tests to test An Ember model, but having a hard time testing computed properties that have a relation dependency (the computed property triggers another model's computed property).
The model that am testing (CoffeeScript):
Customer = DS.Model.extend
firstName: DS.attr('string')
lastName: DS.attr('string')
phones: DS.attr('embedded-list')
phone: (->
#get('phones.firstObject.number')
).property('phones.firstObject.number')
fullName: (->
[#get('lastName'), #get('firstName')].join(' ') )
).property('firstName','lastName')
The meeting Model:
Meeting = DS.Model.extend
customers: DS.hasMany('customer')
startAt: DS.attr('isodate')
status: DS.attr()
objective: DS.attr()
customerPhones: (->
phones = []
#get('customers').forEach (c) ->
c.get('phones').forEach (ph) ->
phones.push(ph.number)
phones
).property('customers.#each.phones')
firstCustomer: (->
#get('customers.firstObject')
).property('customers.firstObject')
firstCustomerFullName: (->
#get('firstCustomer.fullName')
).property('firstCustomer.fullName')
Now, testing customerPhones, and firstCustomerFullName is giving me a real hard time...
My test looks as follows:
`import { test, moduleForModel } from 'ember-qunit';`
moduleForModel('meeting', 'App.Meeting',{
needs: ['model:customer']
setup: ->
Ember.run do (t = #)->
->
customer = t.store().createRecord 'customer', firstName: 'John', lastName: 'Smith', phones:[]
customer.get('phones').addObject(Ember.Object.create({tag: 'home', number: '111222333'}))
customer.get('phones').addObject(Ember.Object.create({tag: 'work', number: '444555666'}))
t.subject().set('customers.content', Ember.ArrayProxy.create({content: []}));
t.subject().get('customers.content').pushObject(customer)
teardown: ->
},(container, context) ->
container.register 'store:main', DS.Store
container.register 'adapter:application', DS.FixtureAdapter
context.__setup_properties__.store = -> container.lookup('store:main')
)
test "it's a DS.Model", -> ok(#subject())
test "attributes: can be created with valid values", ->
meeting = #subject({objective: 'Follow up'})
Ember.run ->
equal(meeting.get('objective', 'Follow up'))
test "properties: firstCustomer & firstCustomerFullName & firstCustomerPhone", ->
meeting = #subject()
Ember.run ->
equal(meeting.get('firstCustomer.fullName'), 'Smith John')
equal(meeting.get('firstCustomer.phone'), '111222333')
Now, I used some techniques in this test, that I found in an answer here on Stack Overflow, but I can't seem to find it now.
That worked perfectly few days ago, now (it seems nonsense I know) whenever I run the test, it errors:
Assertion Failed: You cannot add 'meeting' records to this relationship (only 'meeting' allowed)
I don't know where the error is, nor how to fix it. Spent all the day monkeying around, No outcome.
How can I resolve this?
Okay, what I have so far is too much for a comment, so I'm going to do a WIP Answer.
I removed most of the run loops, they are only necessary for async processes.
I changed some of your computed properties to computed.alias properties
i.e.
phone: (->
#get('phones.firstObject.number')
).property('phones.firstObject.number')
to
phone: Ember.computed.alias('phones.firstObject.number')
I ripped out most of the setup, Ember Data eagerly loads the store on its own and will use fixture ids etc without specifying it. (this part can be put back it, it just isn't necessary in this context).
i.e.
},(container, context) ->
container.register 'store:main', DS.Store
container.register 'adapter:application', DS.FixtureAdapter
context.__setup_properties__.store = -> container.lookup('store:main')
And I apologize in advance, I'm not a fan of coffeescript, so I put it all in js. Now the question is, if you're still seeing any issues we may need to find out what versions of Ember, ED, and Ember Qunit you are using.
http://emberjs.jsbin.com/OxIDiVU/625/edit
I found this question looking for 'how to unit test a computed property that uses a hasMany'.
Here is a simple example of how I did it (thanks to Kitler):
Fridge Model:
foods: DS.hasMany('food', {async: true}),
inDateFoods: Ember.computed('foods.#each.{ignoreEndDate,endDate}', function() {
let foods = this.get('foods');
let now = moment();
return foods.filter(f => f.get(ignoreEndDate) || moment(c.get('endDate')).isAfter(now));
})
So say we now want to test inDateFoods in a unit test? Then do this in your fridge model test file:
import Ember from 'ember';
import { moduleForModel, test } from 'ember-qunit';
import Fridge from '../../../models/fridge';
Fridge.reopen({
foods: Ember.computed(() => [])
});
moduleForModel('fridge', 'Unit | Model | fridge', {
// Specify the other units that are required for this test.
needs: ['model:food']
});
test('filters correctly', function(assert) {
assert.expect(1);
let model = this.subject();
model.pushObject(Ember.Object.create({foods: [{id: 1, ignoreEndDate: false, endDate: '2050-03-08T00:00:00'});
assert.equal(model.get('inDateFoods.length'), 1);
});
They key here is to reopen your model to remove the has many, and push the object after doing this.subject. Before doing the reopen we were getting the error All elements of a hasMany relationship must be instances of DS.Model, you passed [[object Object]] error.
This is driving me nuts. I have a simple data model set up (using Padrino); I'm long past the stage of actually getting any error messages but adding 'App.Repo' models to an 'App.Stack' model just…doesn't work.
App.Store = DS.Store.extend({
revision: 10
adapter: DS.RESTAdapter.create({
bulkCommits: false,
mappings: {
stars: App.Stars,
stacks: App.Stacks
}
})
});
App.Stack = DS.Model.extend({
url: DS.attr('string'),
repos: DS.hasMany('App.Repo')
});
App.Repo = DS.Model.extend({
name: DS.attr('string'),
url: DS.attr('string'),
description: DS.attr('string'),
language: DS.attr('string'),
watchers: DS.attr('number'),
stack: DS.belongsTo('App.Stack'),
stackId: DS.attr('number')
});
var store = App.get('router.store');
newStack = store.createRecord(App.Stack);
console.log(newStack.serialize())
-> Object {url: null} // no mention of a repos array like I was expecting?
newStack.set('url', 'http://google.com');
console.log(newStack.serialize());
-> Object {url: "http://google.com"} // this works though
var repo = App.Repo.find().objectAt(0);
console.log(repo.serialize());
-> Object {name: "floere/james", url: "https://github.com/floere/james", description: "Voice commanded servant for OSX", language: "Ruby", watchers: 97…}
// so this exists too…
repos = newStack.get('repos');
repos.pushObject(repo);
newStack.get('repos.length'); // 1 (repos.toArray() etc etc all work too)
// but then…
console.log(newStack.serialize())
-> Object {url: null}
// and so then I try to save the relationship on the server anyway…
store.commit()
=> {"stack"=>{"url"=>nil}} // in my Ruby server logos
The store is all set up fine talking to my back end (for example submitting a POST to /repo.json sends the correct request); it just doesn't recognise that App.Stack has any relation.
No idea what's going wrong or what to look at for help :(
Also
I tried making the relations in my Ruby console and then accessing them in a view. This is what happens
// in the router
router.get('applicationController').connectOutlet('body', 'stacks', router.get('store').findAll(App.Stack));
// in the view
<script type="text/x-handlebars" data-template-name="stacks">
{{#each stack in controller }}
{{stack.id}} // this works
{{stack.url}} // this works
{{stack.repos.length}} // this returns the correct count
{{#each repo in stack.repos}}
// this loops the right number of times. so there *is* something there. somehow.
{{repo}} // prints out <App.Repo:ember490>
{{repo.id}} // prints out [object Object]
{{/each}}
{{/each}}
On that last note - maybe a clue in the [object Object]?
I'm so lost :(
More Info:
I'm using Padrino with Mongoid, using RABL to give me JSON. As I said, I can query for & template out my Stack & Repo records. Here's a JSON sample for the /stacks.json endpoint
{
"stacks": [
{
"account_id": null,
"id": "50c127ff6f094144ed000001",
"stars": [
{
"description": "Voice commanded servant for OSX",
"id": "50c128996f0941cfe8000001",
"name": "floere/james"
}
]
}
]
}
I think you'll have to add hasMany relationships to your json object manually by looping through the repos array. I'm doing this in my adapter's createRecord method.
createRecord: (store, type, record) ->
data = {}
data[root] = #toData(record, { includeId: true })
repos = []
stack.get("repos").forEach (repo) ->
repos.pushObject repo.serialize()
data[root]["repos"] = repos
...
I've found a way to get embedded related objects in the JSON to load properly. Basically you have to subclass the serializer and then in its initializer you tell it to register a map for the relationship. Here's an example for a model class called Category that has a to-many relationship 'resourceTypes':
App.WOSerializer = DS.Serializer.extend({
init: function(){
this._super();
this.map(App.Category, {
resourceTypes: { embedded: 'load' }
});
}
});
My solution is further explained here.
backbone Model,board:
define([
'underscore',
'backbone',
'collections/lists',
'iobind',
'iosync'
], function( _, Backbone, Lists,ioBind,ioSync) {
var BoardModel = Backbone.Model.extend({
urlRoot: 'board',
noIoBind: false,
socket: io.connect(''),
idAttribute: '_id',
defaults: {
title: 'One Thousand and One Nights'
},
initialize: function() {
this.id = 1;
this.lists = new Lists;
this.socket.emit('joinBoard',this.id);
_.bindAll(this, 'getBoard');
this.ioBind('initBoard', this.getBoard, this);
},
getBoard: function(data){
this.set(data.data.board[0]);
}
});
return BoardModel;
});
backbone View: boardView:
var IndexView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing elements in the HTML.
el: '#board',
// Board template html
template: Mustache.render(Template.board),
events: {
},
initialize: function() {
//Init Data
this.model = new Board();
// var lists = {
// lists: [
// {name: "To Do",
// cards:[
// {name: "Art work for A."},
// {name: "B Prototype."},
// {name: "C prototype."}
// ]
// },
// {name: "Doing",
// cards: [
// {name: "Art work for A."}
// ]
// },
// {name: "Done"}
// ]
// }
// var partial = {card: Template.card_in_list};
// var listHtml = Mustache.render(Template.list,lists,partial);
// template = $(this.template).find('.list-area').append(listHtml);
},
render: function() {
console.log(this.model);
console.log(this.model.toJSON());
var partial = {card: Template.card_in_list};
var listHtml = Mustache.render(Template.list,this.model,partial);
template = $(this.template).find('.list-area').append(listHtml);
this.$el.html(template);
}
});
in View function: render function, the console.log get different result.
console.log(this.model) can get correct object result:
child
_callbacks: Object
_changing: false
_escapedAttributes: Object
_ioEvents: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object
__v: 0
_id: "50b750a7795f285d4e000014"
created: "2012-11-29T12:10:15.269Z"
description: "simple is better, but not simpler"
dueDate: "2012-11-29T12:10:15.269Z"
lists: Array[6]
status: true
title: "test board unique"
__proto__: Object
changed: Object
cid: "c1"
getBoard: function () { [native code] }
id: "50b750a7795f285d4e000014"
lists: child
__proto__: ctor
but this.model.toJSON() only get model default values:
Object
title: "One Thousand and One Nights"
__proto__: Object
it confuse me. anyone know why reason the same model get different result.
In a Backbone Model, your business values (description, title ...) are store in the attributes attribute. When you call toJSON() on your model, what it does is it takes the attributes values, and remove the Backbone.Model object framework's functions and attributes.
When you manually want to set model attributes, you want to use set. I don't know what is in you data.data object, so you should check the doc : http://backbonejs.org/#Model-set
set model.set(attributes, [options])
Set a hash of attributes (one or
many) on the model. If any of the attributes change the models state,
a "change" event will be triggered, unless {silent: true} is passed as
an option. Change events for specific attributes are also triggered,
and you can bind to those as well, for example: change:title, and
change:content. You may also pass individual keys and values.
note.set({title: "March 20", content: "In his eyes she eclipses..."});
book.set("title", "A Scandal in Bohemia"); If the model has a validate
method, it will be validated before the attributes are set, no changes
will occur if the validation fails, and set will return false.
Otherwise, set returns a reference to the model. You may also pass an
error callback in the options, which will be invoked instead of
triggering an "error" event, should validation fail. If {silent: true}
is passed as an option, the validation is deferred until the next
change.
I found i trigger boardView.render twice. when i change code:
a = new boardView;
a.render();
to
a = new boardView;
i got the thing done.
by the way thanks Marcel Falliere's comments.