I'm trying to build a model that dynamically updates Session variables in a Meteor project. I know that plain JSON should not be stored within backbone models, so I have a Special model set up like so:
initialize : function () {
// Log the changed properties
this.on('change', function (model, options) {
for ( var i in options.changes)
this.display(i);
Session.set('NewSpecial', model);
});
},
//Attributes
defaults: {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
},
With "Price" and "Date" being stored in their own models:
//Price model for use within Special
var PriceModel = Backbone.Model.extend({
defaults : {
"Regular" : null,
"Special" : null,
"PercentOff" : null
}
});
//Date model for use within Special
var DateModel = Backbone.Model.extend({
defaults : {
"StartTime" : null,
"EndTime" : null,
"HumanTimeRange" : null
}
});
As shown, when the attributes of the Special model change, it should call display for the attribute that changed, and then set the Session var to the model. If my DateModel or PriceModel change however, it doesn't appear to trigger a change event on the Special model. Should each "DateModel" and "PriceModel" have their own this.on('change', ...) methods that call Special.set(attribute, thisModel) methods? Or is there a different way to go about this?
I see a couple problems.
First of all, your defaults:
defaults: {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
}
That will end up with one PriceModel, one DateModel, and one tags array being shared by all instances of that model. A defaults object is shallow copied and merged into the model's attributes, none of the values in defaults are cloned or duplicated, they're just copied over as-is. If you want distinced Price, Date, and Tags values then use a function for defaults:
defaults: function() {
return {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
};
}
The second problem is that set has a fairly simplistic view of what change means. If you have a look at the source for set, you'll see this:
// If the new and previous value differ, record the change. If not,
// then remove changes for this attribute.
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) !== _.has(prev, attr))) {
this.changed[attr] = val;
if (!silent) this._pending[attr] = true;
} else {
delete this.changed[attr];
delete this._pending[attr];
if (!changing) delete this._changes[attr];
}
The _.isEqual won't recognize that something has changed inside your Price or Date or that you've added or removed something from Tags. If you do things like this:
p = new PriceModel(...);
m.set('Price', p)
then m will noticed that Price has changed but if you:
p = m.get('Price');
p.set(...);
m.set('Price', p);
then m won't recognize that Price has changed; your model won't automatically bind to events on Price so it won't notice the p.set(...) call and it won't recognize m.set('Price', p) as a change since that's little more than a fancy way of saying p = p.
You can solve part of this change problem by not giving set a Tags array that came from get; make a copy, change the copy, and then hand the updated copy to set. The half can be handled by binding to "change" events on the contained Price and Date models and forwarding them similar to how collections do it, something like this:
initialize: function() {
this.attributes.Price.on(
'all',
function(ev, model, opts) { this.trigger(ev, model, opts) },
this
);
//...
}
You'd want to provide your own set implementation in case someone did a set('Price', some_new_object) and you need to rebind your forwarder.
Related
I'm working with a weird data model (no way around it at this point). I'm using restangular to make a rest call to get back a single resource object
Normally, the resource object returned by restangular is just whatever I set my
$scope.resource = response to and I can do resource.name , resource.id in the view/template, etc..
Except this group of resources instead of returning the key, value pairs in the response object, it returns an object within an object like so
resource1: {name: 'value', stuff: 'value', etc}
which is fine because then I would just set my $scope.resource = response.resource1 in my controller
except the problem is, is that there's 5 different kind of resource object names so if I make a resource by id call I might get back resource2, resource4, resource1, etc. so setting my $scope.resource = response.resource1 would only work when I get resource1.
My first attempt to solve this was to just use ng-repeat in which I set
<ul ng-repeat="(resource, value) in resource">
<li class="list-group-item">
Name:
<span class="pull-right"> {{ resource.name }} </span>
</li>
</ul>
which works great except because restangular returns all this extra stuff it's looping through each object it's repeating a bunch of blank html stuff if that makes sense.
My other thought was to try making a constant and make an object that has all 5 resources there and my ng-repeat would only populate based off that constant object (ie: it would check for those strings "resource1, resource2, etc" and if it's there then it will populate the template. But I'm not exactly sure how to do this.
Any other options or are there ng-repeat features i'm just not utilizing? any Help thanks
Here's the example I will be working from. Initially your incoming data looks something like this I believe...
$scope.data = [
{
resource1 : { name: 'r1' }
},
{
resource2 : { name: 'r2' }
},
{
resource2 : { name: 'r2' }
}];
When you receive the data you can normalize it by flattening it out into the following structure...
$scope.normalized = [
{ name : 'r1' },
{ name : 'r2' },
{ name : 'r2' }
];
Or you can add a common field for the object "type"
$scope.expanded = [
{
type : 'resource1',
resource1 : { name: 'r1' }
},
{
type : 'resource2',
resource2 : { name: 'r2' }
},
{
type : 'resource2',
resource2 : { name: 'r2' }
}];
Or you can normalize but retain type data...
$scope.normalizedType = [
{ type : 'resource1', name : 'r1' },
{ type : 'resource2', name : 'r2' },
{ type : 'resource2', name : 'r2' }
];
Normalizing upon retrieval of the data is probably your best bet. The question then becomes do you need to retain the objects type information.
So another solution I came up with was put all the resource key names into a list
resources = ['resource1', 'resource2', 'resource3', 'etc..']
and in my restangular service promise I just checked for which resource number it would be with a for loop like this
return ResourceRestangular.all('resource').get(resourceId).then(function(response){
for (i = 0; i < resources.length ; i++){
if (resources[i] in response){
self.resource = response[resources[i]];
}
}
return self.resource;
No need for ng-repeat anymore!
I am trying to draw a grid where each line is a stock's performance for a single day. In my data structures, I have a Date, a Stock, and a Stock Price resource. The store attached to my grid is the Stock Price store.
So, to the best of my understanding, my biggest problem is that when the grid cell renderers, I need to already have the value, or I need to have a blocking function to get a value.
When I use the getStore() magic function, I'm told the record doesn't know about it (undefined method). I'm assuming it's 'cause the record doesn't have the same functionality as a standalone model.
I see a few ways out of this:
Customise the grid and/or store so that when a load happens, all the related rows are loaded at the same time.
Create a callback in the renderer, and change the cell value afterwards, but I'm ot exactly sure how to do this. I don't actually want to change the cell value (StockId), just the visible output (Symbol).
Change my API to match my view.
Summing these up: #1 seems like a lot of work for a seemingly simple outcome. I keep trying to use the associations, but I'm finding they're not really useful for anything aside from little things here and there, and certainly not for lots of data. #2 I don't quite know where to begin at the moment; and #3 seems like massive overkill, and will generally ruin my server side as I will mean a few more joins, and more complexity when saving records as well.
So, two part question:
Does anyone know how to load a value from an associated model in a grid?
If not, to pique my curiosity, what sort of things are associations used for in any case where there's lots of data to deal with on screen? Lot's of data seems to be the reason to use Ext vs jQueryUI or some other UI framework, so I'm wondering what the associations are for.
Model - Stock Price
Ext.define('MyApp.model.StockPrice', {
extend : 'Ext.data.Model',
idProperty : 'StockPriceId',
fields : [ {
name : 'StockId',
type : 'int'
}, {
name : 'Open',
type : 'float'
}, {
name : 'Close',
type : 'float'
}, {
name : 'DateId',
type : 'date'
}],
proxy : {
type : 'rest',
url : '/api/stock.price'
},
reader : {
type : 'json'
},
associations : [ {
type : 'hasOne',
model : 'MyApp.model.Date',
primaryKey : 'DateId',
foreignKey: 'DateId'
},{
type : 'hasOne',
model : 'MyApp.model.Stock',
primaryKey : 'StockId',
foreignKey : 'StockId'
} ]
});
Model - Stock
Ext.define('MyApp.model.Stock', {
extend : 'Ext.data.Model',
idProperty : 'StockId',
fields : [ {
name : 'StockId',
type : 'int'
}, {
name : 'Symbol',
type : 'string'
} ],
proxy : {
type : 'rest',
url : '/api/stock'
},
reader : {
type : 'json'
},
associations : [ {
type : 'hasMany',
model : 'MyApp.model.StockPick',
primaryKey : 'StockId',
foreignKey : 'StockId'
}]
});
Model - Date
Ext.define('MyApp.model.Date', {
extend : 'Ext.data.Model',
fields : [ 'DateId', 'Date' ]
});
Store - Stock Price
Ext.define('MyApp.store.StockPrice', {
extend : 'Ext.data.Store',
model : 'MyApp.model.StockPrice',
remoteSort : true,
remoteFilter : true,
pageSize : 5,
autoLoad : true
});
View - Stock Price
Ext.define('MyApp.panel.StockData', {
extend : 'Ext.grid.Panel',
store : 'MyApp.store.StockPrice',
columns : [
{
text : 'Symbol',
flex : 1,
sortable : false,
hideable : false,
dataIndex : 'StockId',
renderer : function(stockId, metadata, stockPriceRecord) {
// What goes in here? PROBLEM POINT
MyApp.model.Stock.load(stockId, function() {
// ... some callback
});
// OR
return stockPriceRecord.getStock().get('Symbol');
}
},
{
text : 'Open',
flex : 1,
dataIndex : 'Open',
renderer : 'usMoney'
},
{
text : 'Close',
flex : 1,
dataIndex : 'Close',
renderer : 'usMoney'
},
{
text : 'Volume',
flex : 1,
dataIndex : 'Volume'
}]
});
Your only real option to display data from an associated model in a grid is to use a custom renderer function on the column. This will not change any values; it will simply render the desired output into the cell.
Now, as for implementing that renderer function: I would start by removing the proxies from the models and instead create stores for each model and allow the stores to manage the proxies -- then, attach the store for Stock as a listener on the store for StockPrice to listen for the datachanged event. When the data of the StockPrice store changes, you should grab every unique referenced Stock id and then tell the Stock store to request a payload of stocks with those ids.
That may mean altering your API a little bit to support a SQL IN(...) behind the scenes, but by leaving the joins to the client side you will put less stress on your server side.
In short, you need to use a little bit of all three ideas you came up with in order to best achieve your goal.
I have the following situation. I have a TreeStore that is synced with my server. The server response returns the modified node data with an additional property 'additionalTasks' (which stores some server-generated info for new nodes). How can I get this property's value if all of the store's listeners get already an instantiated record object and not the raw response data ? I've tried adding this additional field to my model, but when I check it's value for the records in the listener function they're shown as null.
Ext.define('ExtendedTask', {
extend: 'Ext.data.NodeInterface',
fields: [
{ name : 'additionalData', type : 'auto', defaultValue : null, persist : false}
]
});
var store = Ext.create("Ext.data.TreeStore", {
model : 'ExtendedTask',
autoSync: false,
proxy : {
method: 'GET',
type : 'ajax',
api: {
read : 'tasks.js',
update : 'update.php',
create : 'update.php'
},
reader : {
type : 'json'
}
},
listeners: {
update : function(){
console.log('arguments: ', arguments);
}
}
});
And this is my update.php response :
<?php
echo '[ { "Id" : 1,'.
'"Name" : "Planning Updated",'.
'"expanded" : true,'.
'"additionalData": ['.
' {'.
' "Name" : "T100",'.
' "parentId" : null'.
' }'.
']'.
']';
?>
The store's proxy always keeps the raw response from the most recent request. Try something like this and see if the information you need is there.
update : function(){
console.log(this.proxy.reader.rawData);
}
I've faced the same issue. What I ended up having to do was use a standard tree model property (I used cls) for whatever custom data you are trying to load into the model. I could be wrong but from the time I took looking to this issue, it seems that extjs is forcing a tree model to only use the standard fields. They state:
If no Model is specified, an implicit model will be created that implements Ext.data.NodeInterface. The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed in the Ext.data.NodeInterface documentation.
However from testing it seems that only the standard fields are available regardless of if a model is specified. Only workaround I could find was to use an a standard string type field which I didn't need like cls, though I'd be interested to see if anyone has found a better way.
http://docs.sencha.com/ext-js/4-1/#!/api/Ext.data.TreeStore
I'd like to display the persistent fields (those defined in my model file) in a property grid.
Property Grid:
Ext.define('ATCOM.view.InspectorProperties', {
extend : 'Ext.grid.property.Grid',
alias : 'widget.inspectorProperties',
cls : 'property-grid',
height : 150,
listeners : {
beforerender : function() {
// Rename the first column
var cols = this.getView().getHeaderCt().getGridColumns();
cols[0].setText("Property");
},
beforeedit : function(e) {
// Read-only
return false;
}
},
source : {} // Start with no items
});
I load items like so in a select event (in a controller), where record is our model object and getInfo() is the property grid:
var source = {};
source.id = record.get('id');
source.start = record.get('start');
source.end = record.get('end');
this.getInfo().setSource(source);
Model:
Ext.define('ATCOM.model.Shift', {
extend : 'Ext.data.Model',
fields : [ 'id', {
name : 'start',
type : 'date',
}, {
name : 'end',
type : 'date',
}, 'position', 'controller' ],
hasMany : {
model : 'ATCOM.model.ShiftAlloc',
name : 'allocations'
}
});
Is there a better way to go about this so all non-associative fields (excluding allocations in my case) are automatically sent to the property grid? It might also be possible to read the fields with ATCOM.model.Shift.getFields() and iterate over those checking for persistent:false; to keep the remaining keys, but how do I get the class reference from an instance - as in, how do I get ATCOM.model.Shift from one of its instances so I can call getFields()?
EDIT:
For finding the class name: http://docs.sencha.com/ext-js/4-1/#!/api/Ext.Base-static-method-getName
It may work to say setSource(record.data). I am just playing with this now; it seems to show the right information, though you may lose control over the details of which fields to enable for editing, etc.
Taking the following Model:
MyModel= Backbone.Model.extend({
defaults : {
myNestedModel:undefined,
},
initialize: function() {
this.set({myNestedModel: new MyNestedModel());
}
});
It has a single property named 'myNestedModel' which has the following definition:
MyNestedModel= Backbone.Model.extend({
defaults : {
myModel:undefined,
}
});
It too has a single Property name 'myModel'. Now if I create an instance of MyModel:
aModel = new MyModel();
The nested model will have been set in MyModel's initialize method. I then use JSON.stringify in a two step process:
// Use Backbone.js framework to get an object that we can use JSON.stringfy on
var modelAsJson = aModel.toJSON();
// Now actually do stringify
var modelAsJsonString = JSON.stringify(modelAsJson);
This works fine and I get the JSON representation of MyModel and it's property of MyNestedModel. The problem occurs when I use defaults, for example:
MyModel= Backbone.Model.extend({
defaults : {
new MyNestedModel(),
}
});
This causes a problem with JSON.stringify since it doesn't support circular references. I assume the circular reference is being created because all instances of MyModel share the same instance of MyNestedModel. Whereas the initialize method creates a new nested model for each instance.
Questions:
Is my understanding of defaults:{} being the 'cause' of the
problem correct?
From a question I posted recently I got the
impression I should be using defaults for all properties. If that is
the case, how should I be using defaults in the scenario presented
in this post/question?
Can someone clarify the use of defaults:{}
with regards to when the value applies, when it's overridden and
whether instances share the same default 'instances'?
Defaults is used only for attributes inside your model ( the data in the model ), and whenever you create your model it takes the values from defaults and sets the attributes. e.g.
User = Backbone.Model.extend({
defaults : {
rating : 0
}
})
User1 = new User({ name : 'jack', email : 'jack#gmail.com' });
User2 = new User({ name : 'john', email : 'john#gmail.com' });
User1.set({ rating : 2 });
Now your two models when called with toJSON will print
{
rating: 2,
name: 'jack',
email: 'jack#gmail.com'
}
{
rating: 0,
name: 'john',
email: 'john#gmail.com'
}
Since defaults is an object, every value you place there is evaluated immediately so :
defaults : {
rating : defaultRating()
}
will call defaultRating() - not everytime when you initialize the model, but immediately ( in the extend method )
You should use defaults for models where you need some data to exist on the creating of the model ( e.g. new myModel() )
In your example you have the following mistakes :
1.set a value without a property
defaults : {
PROPERTY : new Model()
}
2.you don't need such an option for your defaults - you should place there only attributes ( data ) for the model
Defaults applies always as long as it is not replaced by a new defaults in an extended model e.g.
var Model = Backbone.Model.extend({ defaults : { alpha : 'beta' } });
var myModel = Model.extend({ defaults : { beta : 'gama' } });
now your myModel when initialized will have
{ beta : 'gama' } // alpha : 'beta' will not be set as value, because it is replaced