Ember.js binding on array inside a property - arrays

Hello emberjs experts :)
There is something that i don't understand.
Given the following route:
Evibe.MemberShowRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON('/api/user').then(function(user) {
return Ember.Object.create(user);
});
}
});
The call to the api simply returns a user object containing properties. One of this property is an array of picture objects. Like that:
{
username: "A nice user",
pictures: [
{id: 1, is_main: true, url: 'http://www.test.com/img1.jpg'},
{id: 2, is_main: false, url: 'http://www.test.com/img2.jpg'},
{id: 3, is_main: false, url: 'http://www.test.com/img3.jpg'},
{id: 4, is_main: false, url: 'http://www.test.com/img4.jpg'},
]
}
In my controller, i have something like this:
Evibe.MemberShowController = Ember.ObjectController.extend({
nb_pictures: function() {
return this.pictures.length;
}.property('pictures'),
addPictureObject: function(picture) {
this.get('pictures').addObject(picture);
}
});
And in my template, i have something like this:
{{ nb_pictures }} pictures
I don't understand why nb_pictures is not updated, as i'm adding an object into my "pictures" property with the addPictureObject function.
Also, when i try to do something like this:
this.get('pictures').setEach('is_main', false); // Works
this.get('pictures').findBy('id', pictureId).is_main = true; // Doesn't work
this.get('pictures').findBy('id', pictureId).set('is_main', true) // Doesn't work
The first line works as expected.
But... for the second line, i get the error: "Assertion failed: You must use Ember.set() to access this property (of [object Object])"
And for the third one, i get the error: "Uncaught TypeError: Object # has no method 'set' "
Any ideas that can help clarify this would be greatly appreciated.

In your nb_pictures computed property, you have set the dependent key with property('pictures'), the correct is property('pictures.length').
This is the updated code:
Evibe.MemberShowController = Ember.ObjectController.extend({
nb_pictures: function() {
return this.get('pictures.length');
}.property('pictures.length'),
addPictureObject: function(picture) {
this.get('pictures').addObject(picture);
}
});
Using just property('pictures') will make the framework observe just the array replacement, like set('pictures', [...]), not the changes in the array structure get('pictures').pushObject(...). this the reason that your ui don't update.

Related

displaying nested attributes in backgrid

I have a json object in the following format:
{
properties:{
url:"http://..."
}
}
And I want to display the url in a Backgrid grid. However, I can't figure out how to change the name attribute of the column such that it accesses the nested url. I have tried the following examples to no avail:
{
name: "properties.url",
label: "URL",
cell: "uri"
}
And
{
name: "properties[url]",
label: "URL",
cell: "uri"
}
It seems like a simple enough thing to do but I can't find an answer.
Take a look at Backbone's Wiki.
There are at least 4 choices:
backbone-deep-model
backbone-nested
backbone-nestify
backbone-dotattr
This is the entireity of "backbone-dotattr"
(function(_, Backbone) {
_.extend(Backbone.Model.prototype, {
get: function(key) {
return _.reduce(key.split('.'), function(attr, key) {
if (attr instanceof Backbone.Model)
return attr.attributes[key];
return attr[key];
}, this.attributes);
}
});
})(window._, window.Backbone);
with this, i can specify
name: "child.childAttribute"
works perfectly in the "columns" part for Backgrid. hope it helps.

Handling Subsidiary Views in Backbone.js

I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).

How to populate an Ext.data.TreeStore without children or leaf information?

I have a service that returns an array of Things. A Thing simply has an id and a name.
I want to load these into an Ext.tree.Panel. For now, I've defined a data store that mimics a typical response from the service. You can play with a demo JSFiddle here.
Code included below as well:
Ext.define('Thing', {
extend: 'Ext.data.Model',
fields: ['id', 'name'],
});
var store = Ext.create('Ext.data.TreeStore', {
model: 'Thing',
// example service response (in reality, this would be JSON)
root: {
children: [
{
id: 1,
name: 'Thing 1',
},
{
id: 2,
name: 'Thing 2',
},
{
id: 3,
name: 'Thing 3',
},
],
},
listeners: {
append: function(thisNode, newChildNode, index, eOpts) {
if( !newChildNode.isRoot() ) {
newChildNode.set('text', newChildNode.get('name'));
}
},
},
});
Ext.create('Ext.tree.Panel', {
renderTo: Ext.getBody(),
store: store,
rootVisible: false,
});
As you can see, the service only returns a Thing's id and name, no tree node information like children or leaf, which is what an Ext.data.TreeStore normally expects.
I simply cannot change what the service returns as a response. I like that I can call the service and get a flat array of Things, without extraneous tree node information. There are many apps talking to this service that simply do not need such data, and I don't have permission from the powers-that-be to change it.
Unfortunately, without the children element defined for each Thing in the response, Ext raises the following error when I attempt to expand a node (you can produce this yourself in the JSFiddle):
Uncaught TypeError: Cannot call method 'indexOf' of undefined
So, my question is: how can I avoid having to send back children (or leaf) in my response and resolve this error? I'd simply like my Things to be loaded into the tree and to be expandable (yes, they can't be leaves).
Add those information by yourself!
Little example (You easily can try it in the console from your browser):
a = {b:23};
a.c = 25;
//a is now the same like {b:23, c:25}
In the afterRequest-method of the store you can for example do following:
proxy: {
type: 'ajax',
url: 'xxx',
listeners: {
exception: function(proxy, response, options) {
//error case
}
},
afterRequest: function(request, success) {
var resText = request.operation.response.responseText,
allThings = Ext.decode(resText), //Decodes (parses) a JSON string to an object
things = allThins.children, //Now we have an object an we can do what we want...
length = things.length;
for (var i = 0; i < length; i++) {
things[i].leaf = true;
things[i].expanded = true;
}
this.setRootNode(allThings);
}
}
This is just some pseudo-code, but it should work!
Set a debugger Statement (Link) and see what you are really getting back!
I hope this helps!
You might try iterating over your response store and creating children nodes.
Then append your children to your root node
store.each(function(rec) {
var childNode ;
//Create a new object and set it as a leaf node (leaf: true)
childNode = Ext.create('Model_Name', {
id: rec.data.id,
name: rec.data.name,
text : rec.data.name,
leaf : true,
});
// add/append this object to your root node
treepanel.getRootNode().appendChild(childNode );
});
Check this link for more details

Backbone-relational fetchRelated not sending request

I'm using backbone.js and backbone relational 0.5.0 with a Rails 3.2 backend. I have a Card model which has_many Notes.
Here are my JS models and collections:
Workflow.Collections.Cards = Backbone.Collection.extend({
model: Workflow.Models.Card,
url: '/cards'
});
Workflow.Models.Card = Backbone.RelationalModel.extend({
modelName : 'card',
urlRoot : '/cards',
relations: [
{
type: Backbone.HasMany,
key: 'notes',
relatedModel: 'Workflow.Models.Note',
collectionType: 'Workflow.Collections.Notes',
includeInJSON: false,
reverseRelation: {
key: 'card',
includeInJSON: 'id'
}
}]
});
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note,
url: '/cards/74/notes' // intentionally hard-coded for now
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : '/notes'
});
Normal fetching works great, but when I try fetchRelated in the console, I get an empty array:
card = new Workflow.Models.Card({id: 74}) // cool
card.fetch() // hits the sever with GET "/cards/74" - works great
card.fetchRelated('notes') // [] - didn't even try to hit the server
What's weird is that this works:
card.get('notes').fetch() // cool - GET "/cards/74/notes"
I could use that method and parse the response text, but it feels really dirty.
Anyone know what I'm missing here?
Thanks in advance, this one is really torturing me!
Stu
You should create Card with Note ids array: card = new Workflow.Models.Card({id: 74, notes: [74, 75]}); and change the url method of Notes accordingly:
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : function () {
return this.get('card').url() + '/notes';
}
});
card = new Workflow.Models.Card({id: 74, notes: [74, 75]});
card.fetchRelated('notes');
http://jsfiddle.net/theotheo/5DAzx/
I should have posted my solution a while back - there might well be a better way, but this is the convention I've gone with:
All of the following code is in the card view (which is where the notes are displayed).
First, I bind a renderNotes method to the 'reset' event on the card's notes collection:
initialize: function () {
_.bindAll(this);
this.model.get('notes').on('reset', this.renderNotes);
var self = this;
this.model.get('notes').on('add', function(addedNote, relatedCollection) {
self.renderNote(addedNote);
});
}
I also bind to the 'add' on that collection to call a singular renderNote.
The renderNotes and renderNote methods work like this:
renderNotes: function () {
if (this.model.get('notes')) {
this.model.get('notes').each(this.renderNote);
}
},
renderNote: function (note) {
var noteView = new Workflow.Views.Note({ model: note });
this.$('.notes').append(noteView.render().el);
},
Then, the last piece of the puzzle is to actually hit the server up for the card's notes (which will in turn fire the 'reset' event I bound to above). I do this in the card view's render method:
render: function () {
// render all of the eager-loaded things
this.model.get('notes').fetch();
return this;
},
As #user1248256 kindly helped me work out in the comments on my OP, the confusion was mainly in that I expected fetchRelated to pull down lazy-loaded records - that's actually not the case.
As a side-note, this view is actually a modal and be opened and closed (removed from the page). To prevent the zombie events problem described in this excellent post, I also manually unbind the events mentioned above.

Return extra data besides tree data from ExtJS TreeLoader dataUrl?

I asked this question in the Ext JS forums, but I received no responses, so I am asking here.
I have a TreePanel (code below) that uses a TreeLoader and an AsyncTreeNode. In my API method specified by the TreeLoader's dataUrl, I return a JSON array to populate the tree.
This works great, of course. However, I need to return an additional item--an integer--in addition to the array, and I need to display that value somewhere else in my UI. Is this possible? If not, what else would be a good solution?
Here's the code I have currently:
tree = new Ext.tree.TreePanel({
enableDD: true,
rootVisible: false,
useArrows: true,
loader: new Ext.tree.TreeLoader({
dataUrl: '/api/method'
}),
root: new Ext.tree.AsyncTreeNode()
});
I want to return one single integer value for the entire response--not per node. Basically my API method will create a database record, and I need to return a value from that database record.
EDIT Thanks to Mike, I have solved this problem. I extended the Ext.tree.TreeLoader class like so:
TreeLoaderWithMetaData = Ext.extend(Ext.tree.TreeLoader, {
processResponse : function(response, node, callback) {
var json = response.responseText;
try {
var o = eval("("+json+")");
metaData = o.shift();
node.beginUpdate();
for(var i=0, len=o.length; i<len; i++) {
var n = this.createNode(o[i]);
if (n) {
node.appendChild(n);
}
}
node.endUpdate();
if(typeof callback == "function") {
callback(this, node);
}
}
catch (e) {
this.handleFailure(response);
}
}
});
And then I can reference variables in the meta data like public members: metaData.my_variable1, metaData.my_variable2. My AJAX data from the server just has an extra array item:
[{"my_variable1":"value1","my_variable2":"value2"},{"id":"node1","text":"Node 1",children:[{"id":"node1nodeA","text":"Node 1 Node A"}]]
You need to override the processResponse function in TreePanel and then you'll be able to return whatever format JSON you'd like:
From the ExtJS forums:
http://www.extjs.com/forum/showthread.php?32772-Passing-JSON-string-from-Grails-to-populate-TreePanel
The code at the bottom of that thread will help you.
As far as i understood, you want to pass additional parametrs with json and display it somewhere else when tree is loaded.
In this case you can simply return from server modified JSON like this
[{
id: 1,
yourParmam : 'val',
text: 'A leaf Node',
leaf: true
},{
id: 2,
yourParmam : 'val',
text: 'A folder Node',
children: [{
id: 3,
yourParmam : 'val',
text: 'A child Node',
leaf: true
}]
}]
Then subscribe to even load : ( Object This, Object node, Object response ) and simply parse response to find out you parm and do whatever you need

Resources