I would like to save calls to my server, so I am currently using Model.save() with the patch option and sending changedAttributes().
I would like to remove an attribute and add a new one. Model.set()/unset() will modify changedAttributes() each time such that I cannot use it with the Model.save()/patch scheme described above.
I think I would like to simply call Model.set() and pass in an object with the values I wish to unset set to undefined along with the values I wish to set.
Is there a way that I can unset() and set() in one go to get the changedAttributes()? Or maybe determine the changedAttributes() for a combined set of operations?
// Currently
var m = new Backbone.Model({ "foo": "bar" });
m.unset("foo");
console.log(m.changedAttributes()); // { "foo": undefined }
m.set("baz", "bar");
console.log(m.changedAttributes()); // { "baz": "bar" }
console.log(m.attributes); // { "baz": "bar" }
// At this point, how do I get the combination of changed attributes? something like: { "foo": undefined, "baz": "bar" }?
// Is that even possible? Am I doing something horribly wrong?
//================================================================
// What (I think) I want is for Model.set() to remove attributes with values of undefined, so I only have to make one call and changedAttributes() will be pristine. Maybe with a option or something?
var w = new Backbone.Model({ "foo": "bar" });
w.set({ "foo": undefined, "baz": "bar" });
console.log(w.changedAttributes()); // { "foo": undefined, "baz": "bar" }
console.log(w.attributes); // I would like it to be { "baz": "bar" }, "foo" having been removed in the set() call.
//================================================================
// I was trying to avoid processing the objects by hand. I realize that I can do something like the following.
var h = new Backbone.Model({ "foo": "bar" });
var changes = { "foo": undefined, "baz": "bar" };
_.each(changes, function(val, key) {
if (_.isUndefined(val)) {
h.unset(key, { "silent": true });
} else {
h.set(key, val, { "silent": true });
}
});
h.trigger('change'); // Trigger a change event after all the changes have been done.
console.log(changes); // { "foo": undefined, "baz": "bar" }
console.log(h.attributes); // { "baz": "bar" }
Fiddle of above code in action: http://jsfiddle.net/clayzermk1/AmBfh/
There seems to have been some discussion on this topic about a year ago https://github.com/documentcloud/backbone/pull/879. It seems like the functionality I wanted existed at some point.
EDIT: As #dennis-rongo pointed out, I can obviously do this by hand. To restate my question above: "Does Backbone allow setting/deleting of attributes at once?" and if not, what is the rationale behind that decision? Derick Bailey created Backbone.Memento (https://github.com/derickbailey/backbone.memento) to deal with attribute states, and there are several issues on Backbone about model states closely related to this scenario (https://github.com/documentcloud/backbone/pull/2360, somewhat relevant: https://github.com/documentcloud/backbone/issues/2316, highly relevant: https://github.com/documentcloud/backbone/issues/2301).
EDIT 2: I'm not looking for a hand-rolled solution, I can make it do more or less what I want (see sample code above). I'm looking for a justification of the current design with a clean example for this common scenario - set and unset in one go.
UPDATE: There has been some conversation about this subject in https://github.com/documentcloud/backbone/issues/2301. I have submitted a pull request (https://github.com/documentcloud/backbone/pull/2368) to try and encourage discussion of the current implementation.
Thank you to everyone who posted an answer!
There's a lot of ways to skin this one! So, I'll focus on your the part of your question where you ask:
Is there a way that I can unset() and set() in one go to get the changedAttributes()?
because I think that's the way to go here.
Backbone.Model.unset() is just an alias for Backbone.Model.set(). From the source:
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
So why not just do m.set({"baz": "bar", "foo": void 0});? See this fiddle I forked from yours: http://jsfiddle.net/dimadima/Q8ZuV/. Pasting from there, the result will be
console.log(m.changedAttributes()); // { "baz": "bar", "foo": undefined }
console.log(m.attributes); // // {foo: undefined, baz: "bar"}, unfortunately "foo"is not deleted
So m.attributes is a bit off because the key you've unset hasn't been deleted, but you can test for that.
Anyway, I recommend skimming the source of Backbone.Model.set() to get a sense of what your other options would be. I could elaborate if you'd like.
Something like this should work.
Basically loop through all the attributes and unset properties that are invalid (falsy and non-boolean values).
/** Create a Model and set attributes */
var President = Backbone.Model.extend({});
var m = new President({first: 'Abraham', last: 'Lincoln', age: 90});
/** Blank out an attribute or two */
m.set({age: ''});
/** Loop through each attributes and unset falsy ones.
Also pass in silent = true so it doesn't trigger a change event
(in case you have listeners).
*/
_.each(m.toJSON(), function(val, col){
if (typeof val !=='boolean' && !val) {
m.unset(col, {silent: true});
}
}, this);
/** Output the new Model */
console.log(m.toJSON());
OR
You can create a new Model that only contains the changed attributes if you'd rather go in that direction.
var President = Backbone.Model.extend({});
var m = new President({
first: 'Abraham', last: 'Lincoln', age: 90, registered: true});
/** Blank out or change an attribute or two */
m.set({first: null});
/** Pass changed attributes to a new Model */
var t = new President();
if (!_.isEmpty(m.changedAttributes())) {
_.each(m.changedAttributes(), function(val, col) {
t.set(col, val);
}, this);
}
The current implementation will allow you to call set() and pass in an object with mixed attributes to set and unset. To effectively unset an attribute, assign it the value of undefined. The key will remain in the attributes hash on the model, but any time the model is serialized to JSON, the undefined values will not be present in the serialization (this is due to the implementation of JSON.stringify()). It will not delete the attributes from the model.
The behavior of JSON.stringify() removing undefined values in serialization is described on MDN - JSON - stringify:
If undefined, a function, or an XML value is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array).
I was not using JSON serialization for my specific case (BSON), so I ended up having to hand-code a solution for myself.
I struck up a discussion on GitHub with a pull request, in the end a decision was made to keep the API as it is. For details see this pull requtest: https://github.com/documentcloud/backbone/pull/2368.
Thank you again to everyone who participated in the discussion, both on SO and GH.
Related
When I update a model, waterlock .update() always return an array of objects, even if I set on criteria a primaryKey.
on my code
Ad.update({ id: req.param('id') }, {
// desired attributed to be updated
}).exec(function(err, updatedRecord) {
// updatedRecord is always an array of objects
});
And in order to use the updatedRecord, I have to point out to 0 index like updatedRecord[0] which is something I consider not very clean. According to docs update() in sails, this is a common escenario.
Knowing that, I have 2 questions:
Wouldn't be better that when you find one model return just a updated object for that model, not an array?
If that is a convention, how could be overrided this function in order to return just an object instead of an array when .update() have only affected one record?
it is a convention that it will update all the records that matches the find criteria, but as you are probably using a unique validation on model, it will probably return an array of 1 or 0. You need to do it on hand.
You can override methods in model, by implementing a method with same name as waterline default. But as you will need to completely rewrite the code, it is not viable. Neither changing waterline underlying code.
A solution will be creating a new function on your Ad model:
module.exports = {
attributes: {
adid: {
unique: true,
required: true
},
updateMe: {
}
},
updateOne: function(adid, newUpdateMe, cb){
Ad.update({ id: req.param('id') }, {
// desired attributed to be updated
}).exec(function(err, updatedRecord) {
// updatedRecord is always an array of objects
if (updatedRecord.length == 1){
return cb(null, updatedRecord[0]);
}
return cb(null, {}); //also can error if not found.
});
}
};
Also. Avoid using id as an model attribute (use other name), as some databases like mongodb already add this attribute as default and may cause conflicts with your model.
I dont think its possible with waterline. Its because update method is a generalized one, passing a primary key in where condition is always not the case.
So I'm using this Rest API with ngResource to do get, query, post and update requests. What I'm looking for, is a way to define the structure for each entity.
For example, assuming we have:
module.factory('app.entity.item', function($resource) {
return $resource('http://xmpl.io/items/:itemId', { itemId: '#id' });
});
I want to instantiate it in a controller like:
module.controller('AddItemCtrl', ['app.entity.item', function(Item) {
$scope.item = new Item();
});
and bind it to the respective form in my template.
The actual problem that I have run into, is that I have to deal with 1:m tables.
An example of the entity structure would be:
{
"name": "",
"categories": [],
"list": [
{
"value": "",
"list": [
{
"value": "",
"list": [
{
"value": ""
}
]
}
]
}
]
}
(A more thorough example in the fiddle below)
Now the first two fields are obviously not the problem. It is the third one. The list. Each one of these lists can have a variable number of items.
I am currently using ngRepeat and an add(type, context) method, which adds a new set of fields to the scope (value field in this example and child lists for the first two levels), which will appear in UI by ngRepeat so the user can fill it up and submit it to the service.
First off, I have to define the structure, so the UI would not be empty when the page loads.
module.controller('AddItemCtrl', ['app.entity.item', function(Item) {
$scope.item = new Item({
"name": "",
"categories": [],
"list": [
{
"value": "",
"list": [
{
"value": "",
"list": [
{
"value": ""
}
]
}
]
}
]
});
});
But that is redundant. I have to do it everywhere!
Another issue is that when the item.$save is called, the model is emptied (perhaps re-instantiated?) and the fields inside the list property (managed by the ngRepeat directive) are gone.
So I'm wondering, what would you do under such circumstances.
Is there a way to define the entity (resource) structure?
SAMPLE: http://jsfiddle.net/g15sqd5s/3/
trying to give simple answer - for simple structures I would use something like
module.factory('Item', function($resource) {
var resource = $resource('http://xmpl.io/items/:itemId', { itemId: '#id' },
// you can also define transformRequest here:
{ transformRequest: function(data) {
// data can be transformed here
return angular.toJson(data);
}});
return angular.extend(resource.prototype,
{
name: null,
categories: []
});
});
but then be aware of need to 'flatten' the object.
and for the more complex model I would check restangular
similar topic is also discussed here:
How can I extend the constructor of an AngularJS resource ($resource)?
I would go ahead and revise my model structure in the backend in the first place - the models on the client side should merely follow the ones already defined, rather than being re-defined in a transform block. So, to answer your question, the "default" model structure comes from the server. What you get in your $resource objects has the structure of what your server returns.
To start off, is it really ok to invoke $save on the Item model when the user has populated some values? What we want to save are obviously the lists associated with an item, not the item itself. A separate resource defined in the backend, say items/<item_id>/list, may be a cleaner solution. It may not scale very well, as you'll have to make a separate GET request for each item to fetch its list, but that's the proper RESTful way to do it.
Extending this approach to the example in your fiddle, I imagine a routing scheme like buildings/<building_id>/floors/<floor_id>/units/<unit_id> would be a proper solution. Making a GET request to buildings/ should yield you a list of buildings; each building in the array returned should be an instance of a Building model, which has the proper URL set so the user can perform a single POST and update only the building name, instead of sending back the whole structure back to the server. Applying this recursively to the nested resources should give you a clean and concise way to deal with model changes.
Regarding the UI part - I would go ahead and define three directives for buildings, floors and units, and let each one manage an array with the respective resources, also taking care for the UI bindings to the model values.
So how could a Building model look like?
var BuildingResource = $resource('/buildings/:id', { id: '#id' });
Invoking BuildingResource.query() should yield an array of existing buildings. Adding a new building could look like this:
var newBuilding = new BuildingResource();
newBuilding.$save().then(function(building) {
$scope.buildings.push(building);
}, function(errData) {
//Handle error here...
});
It should be easy to extend this pattern for the rest of the resources - note that what the server needs to return for every building is just the name and the id; knowing the id is sufficient to construct an URL (and a $resource object, respectively) to fetch the needed child resources (in this case, floors).
Suppose I'm working with an API which returns JSON data, but which has a complex or variable structure. For example, a string-valued property may be a plain literal, or may be tagged with a language:
/* first pattern */
{ "id": 1,
"label": "a foo"
}
/* second pattern */
{ "id": 2,
"label": [ {"value": "a foo", "lang": "en"},
{"value": "un foo", "lang": "fr"}]
}
In my client-side code, I don't want to have view code worrying about whether a label is available in multiple-languages, and which one to pick, etc. Or I might want to hide the detailed JSON structure for other reasons. So, I might wrap the JSON value in an object with a suitable API:
/** Value object for foo instances sent from server */
var Foo = function( json ) {
this.json = json;
};
/** Return a suitable label for this foo object */
Foo.prototype.label = function() {
var i18n = ... ;
if (i18n.prefLang && _.isArray(this.json.label)) // ... etc etc
};
So this is all pretty normal value-object pattern, and it's helpful because it's more decoupled from the specific JSON structure, more testable, etc. OK good.
What I currently don't see a way around is how to use one of these value objects with Backbone and Marionette. Specifically, I'd like to use a Foo object as the basis for a Backbone Model, and bind it to a Marionette ItemView. However, as far as I can see, the values in a Model are taken directly from the JSON structure - I can't see a way to recognise that the objects are functions:
var modelFoo = new Backbone.Model( foo );
> undefined
modelFoo.get( "label" ).constructor
> function Function() { [native code] }
So my question is: what is a good way to decouple the attributes of a Backbone Model from the specifics of a given JSON structure, such as a complex API value? Can value objects, models and views be made to play nice?
Edit
Let me add one more example, as I think the example above focussing on i18n issues only conveys part of my concern. Simplifying somewhat, in my domain, I have waterbodies comprising rivers, lakes and inter-tidal zones. A waterbody has associated with it one or more sampling points, and each sampling point has a latest sample. This might come back from the data API on the server as something like:
{"id": "GB12345678",
"centre": {"lat": 1.2345, "long": "-2.3456"},
"type": "river",
"samplingPoints": [{"id": "sp98765",
"latestSample": {"date": "20130807",
"classification": "normal"}
}]
}
So in my view code, I could write expressions such as:
<%= waterbody.samplingPoints[0].latestSample.classification %>
or
<% if (waterbody.type === "river") { %>
but that would be horrible, and easily broken if the API format changes. Slightly better, I could abstract such manipulations out into template helper functions, but they are still hard to write tests for. What I'd like to do is have a value object class Waterbody, so that my view code can have something like:
<%= waterbody.latestClassification() %>
One of the main problems I'm finding with Marionette is the insistence on calling toJSON() on the models passed to views, but perhaps some of the computed property suggestions have a way of getting around that.
The cleanest solution IMO is to put the label accessor into the model instead of the VO:
var FooModel = Backbone.Model.extend({
getLabel : function(){
return this.getLocalized("label");
},
getLocalized : function(key){
//return correct value from "label" array
}
});
and let the views use FooModel#getLabel instead of FooModel#get("label")
--EDIT 1
This lib seems interesting for your use case as well: Backbone.Schema
It allows you to formally declare the type of your model's attributes, but also provides some syntax sugar for localized strings and allows you to create dynamic attributes (called 'computed properties'), composed from the values of other attributes.
--EDIT 2 (in response to the edited question)
IMO the VO returned from the server should be wrapped inside a model and this model is passed to the view. The model implements latestClassification, not the VO, this allows the view to directly call that method on the model.
A simple approach to this (possibly to simple for your implementation) would be to override the model's parse method to return suitable attributes:
var modelFoo = Backbone.Model.extend({
parse: function ( json ) {
var i18n = ... ;
if (i18n.prefLang && _.isArray(json.label)) {
// json.label = "complex structure"
}
return json;
}
});
That way only your model worries about how the data from the server is formatted without adding another layer of abstraction.
I wanted to update the rank attribute of an existing model which I passed from another view. However, I get the error Uncaught TypeError: Object # has no method 'set'.
In the initialize part of the view, I have :
this.collection = new tgcollection({model : this.options.model });
I define a function updateModel intended to update the attribute value as:
updateModel: function(){
var val= $("#textbox_id").val();
console.log(val);
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
this.options.model.set({"rank": val});
this.render();
//
},
Where am I going wrong?
I can see the value and the model with its previous attribute values in the console.
The model:
define(['jquery','underscore', 'backbone', 'deepmodel'],
function($,_, Backbone) {
var model = Backbone.DeepModel.extend({
// Default attributes for the model.
defaults : {
id: null,
rank: null,
},
initialize: function(){
_.bindAll(this,"update");
this.bind('change : cost', this.update);
},
update: function(){
console.log(this.get("cost"));
},
// Remove this model from *localStorage*.
clear : function() {
this.destroy();
},
});
return model;
});
Just do
this.model.set({"rank": val});
instead of
this.options.model.set({"rank": val});
The model within a view is accessed via this.model not this.options.model
I love a good mystery. Here is my best guess based on what I see. The problem is probably even further back. Where you call:
this.collection = new tgcollection({model : this.options.model });
this.options.model is probably not what you think it is. It would be helpful to see the view BEFORE this view that is instantiating and passing in this.options.model. BTW, with models and collections passed into the view, you can always shorten it to this.model Model, Collection and a handful of others are special in that they get attached directly to the View once passed in.
I'm assuming that in your updateModel() the following SEEM to work:
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
The error is coming up on the set(), not the lines above. So the assumption is that you passed in a model. Or did you? My wild guess is that what this.options.model actually is, is just a json object of your model. This might explain why you "see" the model in your console when you stringify it, but then Backbone protests when you call set() on it.
Instead of JSON.stringify to test this.options.model try just console.log(this.options.model). Well, you don't have to test really. The fact that Backbone can't find set() on this object is a tell tale sign. If you're not seeing the complexity of a Backbone model in your console - it's not a model.
Also, for testing and debugging particularly models, I tend to use the model.toJSON() function as a quick check that it's a model and I'm seeing attributes I expect.
Let us know if you have more clues.
now that I managed to get random AJAX output, I want to get some useful values from the database as the next step.
Once again, my AJAX call looks like this (additionally, I added a JSON call, which would be even better).
$.ajax({
url: "index.php",
data: "tx_myext_myplugin1[controller]=Mycontroller1&tx_myext_myplugin1[action]=ajax&type=89657201",
success: function(result) {
alert(result);
}
});
/*
var uri = '<f:uri.action action="ajax" controller="Mycontroller1" pageType="89657201" />';
jQuery.getJSON(uri, function(result) {
alert(result.c);
});
*/
my ajaxAction function:
public function ajaxAction() {
$arr = array ('a'=>1,'b'=>2,'c'=>3,'d'=>4,'e'=>5);
return json_encode($arr);
}
This works when I use the JSON call, now I need to get an array with database values though. I thought calling the repository with a findAll() function would already help, but it's not an array I think, which is why I can't use it. A different idea would be to use the getValue methods I wrote in the Model, but I'm not sure if this would help.
Disclaimer: Generally using findAll() method can be real performance killer therefore try to write custom finders, selecting only required properties, especcialy if your model is big or contains many relations!
You are close enough, as you can send findAll() result with json_encode(), but be careful, depending on your model, json created from findAll can be really huge. It's better idea to iterate results and rewrite to new array only required values.
$dataFromRepo = $this->yourRepository->findAll();
$resultArray = array();
foreach ($dataFromRepo as $object){
$resultArray[$object->getUid()] = $object->getTitle();
}
return json_encode($resultArray);
in result you'll get basic JSON object:
{
"1": "Title of first item",
"2": "Second item",
"3": "Et cetera"
}
When you'll remove custom index from $resultArray
foreach ($dataFromRepo as $object){
$resultArray[] = $object->getTitle();
}
you will get JSON array
[
"Title of first item",
"Second item",
"Et cetera"
]
And so on. Of course you can also build this way multidimensional array and send more sophisticated objects to get all you need at once.
P.S. Try always to use for an example JsonLint - online validator to validate if the output you're expecting is valid.