steal('can/util', 'can/map', 'can/list', function (can) {
/** @add can.Model **/
steal('can/util', 'can/map', 'can/list', function (can) {
/** @add can.Model **/
pipe
lets you pipe the results of a successful deferred
through a function before resolving the deferred.
var pipe = function (def, thisArg, func) {
The piped result will be available through a new Deferred.
var d = new can.Deferred();
def.then(function () {
var args = can.makeArray(arguments),
success = true;
try {
Pipe the results through the function.
args[0] = func.apply(thisArg, args);
} catch (e) {
success = false;
The function threw an error, so reject the Deferred.
d.rejectWith(d, [e].concat(args));
}
if (success) {
Resolve the new Deferred with the piped value.
d.resolveWith(d, args);
}
}, function () {
Pass on the rejection if the original Deferred never resolved.
d.rejectWith(this, arguments);
});
can.ajax
returns a Deferred with an abort method to halt the AJAX call.
if (typeof def.abort === 'function') {
d.abort = function () {
return def.abort();
};
}
Return the new (piped) Deferred.
return d;
},
When new model constructors are set up without a full name,
modelNum
lets us name them uniquely (to keep track of them).
modelNum = 0,
getId = function (inst) {
can.__observe
makes a note that id
was just read.
can.__observe(inst, inst.constructor.id);
Use __get
instead of attr
for performance. (But that means we have to remember to call can.__observe
.)
return inst.___get(inst.constructor.id);
},
ajax = function (ajaxOb, data, type, dataType, success, error) {
var params = {};
A string here would be something like "GET /endpoint"
.
if (typeof ajaxOb === 'string') {
Split on spaces to separate the HTTP method and the URL.
var parts = ajaxOb.split(/\s+/);
params.url = parts.pop();
if (parts.length) {
params.type = parts.pop();
}
} else {
If the first argument is an object, just load it into params
.
can.extend(params, ajaxOb);
}
If the data
argument is a plain object, copy it into params
.
params.data = typeof data === "object" && !can.isArray(data) ?
can.extend(params.data || {}, data) : data;
Substitute in data for any templated parts of the URL.
params.url = can.sub(params.url, params.data, true);
return can.ajax(can.extend({
type: type || 'post',
dataType: dataType || 'json',
success: success,
error: error
}, params));
},
makeRequest = function (modelObj, type, success, error, method) {
var args;
If modelObj
is an Array, it it means we are coming from
the queued request, and we’re passing already-serialized data.
if (can.isArray(modelObj)) {
In that case, modelObj’s signature will be [modelObj, serializedData]
, so we need to unpack it.
args = modelObj[1];
modelObj = modelObj[0];
} else {
If we aren’t supplied with serialized data, we’ll make our own.
args = modelObj.serialize();
}
args = [args];
var deferred,
model = modelObj.constructor,
jqXHR;
When calling update
and destroy
, the current ID needs to be the first parameter in the AJAX call.
if (type === 'update' || type === 'destroy') {
args.unshift(getId(modelObj));
}
jqXHR = model[type].apply(model, args);
Make sure that can.Model can react to the request before anything else does.
deferred = pipe(jqXHR, modelObj, function (data) {
method
is here because "destroyed" !== "destroy" + "d"
.
TODO: Do something smarter/more consistent here?
modelObj[method || type + "d"](data, jqXHR);
return modelObj;
});
Hook up abort
if (jqXHR.abort) {
deferred.abort = function () {
jqXHR.abort();
};
}
deferred.then(success, error);
return deferred;
},
converters = {
The default function for converting into a list of models. Needs to be stored separate
because we will reference it in models static setup
, too.
models: function (instancesRawData, oldList, xhr) {
Increment reqs counter so new instances will be added to the store. (This is cleaned up at the end of the method.)
can.Model._reqs++;
If there is no data, we can’t really do anything with it.
if (!instancesRawData) {
return;
}
If the “raw” data is already a List, it’s not raw.
if (instancesRawData instanceof this.List) {
return instancesRawData;
}
var self = this,
tmp
will hold the models before we push them onto modelList
.
tmp = [],
ML
(see way below) is just can.Model.List
.
ListClass = self.List || ML,
modelList = oldList instanceof can.List ? oldList : new ListClass(),
Check if we were handed an Array or a model list.
rawDataIsList = instancesRawData instanceof ML,
Get the “plain” objects from the models from the list/array.
raw = rawDataIsList ? instancesRawData.serialize() : instancesRawData;
raw = self.parseModels(raw, xhr);
if(raw.data) {
instancesRawData = raw;
raw = raw.data;
}
if (typeof raw === 'undefined' || !can.isArray(raw)) {
throw new Error('Could not get any raw data while converting using .models');
}
!steal-remove-start
if (!raw.length) {
can.dev.warn("model.js models has no data.");
}
!steal-remove-end
If there was anything left in the list we were given, get rid of it.
if (modelList.length) {
modelList.splice(0);
}
If we pushed these directly onto the list, it would cause a change event for each model.
So, we push them onto tmp
first and then push everything at once, causing one atomic change event that contains all the models at once.
can.each(raw, function (rawPart) {
tmp.push(self.model(rawPart, xhr));
});
modelList.push.apply(modelList, tmp);
If there was other stuff on instancesRawData
, let’s transfer that onto modelList
too.
if (!can.isArray(instancesRawData)) {
can.each(instancesRawData, function (val, prop) {
if (prop !== 'data') {
modelList.attr(prop, val);
}
});
}
Clean up the store on the next turn of the event loop. (this
is a model constructor.)
setTimeout(can.proxy(this._clean, this), 1);
return modelList;
},
model: function (attributes, oldModel, xhr) {
If there’re no properties, there can be no model.
if (!attributes) {
return;
}
If this object knows how to serialize, parse, or access itself, we’ll use that instead.
if (typeof attributes.serialize === 'function') {
attributes = attributes.serialize();
} else {
attributes = this.parseModel(attributes, xhr);
}
var id = attributes[this.id];
Models from the store always have priority 0 is a valid ID.
if((id || id === 0) && this.store[id]) {
oldModel = this.store[id];
}
var model = oldModel && can.isFunction(oldModel.attr) ?
If this model is in the store already, just update it.
oldModel.attr(attributes, this.removeAttr || false) :
Otherwise, we need a new model.
new this(attributes);
return model;
}
},
This object describes how to take the data from an AJAX request and prepare it for models
and model
.
These functions are meant to be overwritten (if necessary) in an extended model constructor.
makeParser = {
parseModel: function (prop) {
return function (attributes) {
return prop ? can.getObject(prop, attributes) : attributes;
};
},
parseModels: function (prop) {
return function (attributes) {
if(can.isArray(attributes)) {
return attributes;
}
prop = prop || 'data';
var result = can.getObject(prop, attributes);
if(!can.isArray(result)) {
throw new Error('Could not get any raw data while converting using .models');
}
return result;
};
}
},
This object describes how to make an AJAX request for each ajax method (create
, update
, etc.)
Each AJAX method is an object in ajaxMethods
and can have the following properties:
url
: Which property on the model contains the default URL for this method.type
: The default HTTP request method.data
: A method that takes the arguments from makeRequest
(see above) and returns a data object for use in the AJAX call. ajaxMethods = {
create: {
url: "_shortName",
type: "post"
},
update: {
data: function (id, attrs) {
attrs = attrs || {};
this.id
is the property that represents the ID (and is usually "id"
).
var identity = this.id;
If the value of the property being used as the ID changed, indicate that in the request and replace the current ID property.
if (attrs[identity] && attrs[identity] !== id) {
attrs["new" + can.capitalize(id)] = attrs[identity];
delete attrs[identity];
}
attrs[identity] = id;
return attrs;
},
type: "put"
},
destroy: {
type: 'delete',
data: function (id, attrs) {
attrs = attrs || {};
this.id
is the property that represents the ID (and is usually "id"
).
attrs.id = attrs[this.id] = id;
return attrs;
}
},
findAll: {
url: "_shortName"
},
findOne: {}
},
Takes a method defined just above and a string that describes how to call that method and makes a function that calls that method with the given data.
ajaxMethod
: The object defined above in ajaxMethods
.str
: The string the configuration provided (such as "/recipes.json"
for a findAll
call). ajaxMaker = function (ajaxMethod, str) {
return function (data) {
data = ajaxMethod.data ?
If the AJAX method mentioned above has its own way of getting data
, use that.
ajaxMethod.data.apply(this, arguments) :
Otherwise, just use the data passed in.
data;
Make the AJAX call with the URL, data, and type indicated by the proper ajaxMethod
above.
return ajax(str || this[ajaxMethod.url || "_url"], data, ajaxMethod.type || "get");
};
},
For each of the names (create, update, destroy, findOne, and findAll) use the
URL provided by the resource
property. For example:
ToDo = can.Model.extend({
resource: "/todos"
}, {});
Will create a can.Model that is identical to:
ToDo = can.Model.extend({
findAll: "GET /todos",
findOne: "GET /todos/{id}",
create: "POST /todos",
update: "PUT /todos/{id}",
destroy: "DELETE /todos/{id}"
},{});
model
: the can.Model that has the resource propertymethod
: a property from the ajaxMethod object createURLFromResource = function(model, name) {
if (!model.resource) { return; }
var resource = model.resource.replace(/\/+$/, "");
if (name === "findAll" || name === "create") {
return resource;
} else {
return resource + "/{" + model.id + "}";
}
};
/** @static */
can.Model = can.Map.extend({
fullName
identifies the model type in debugging.
fullName: "can.Model",
_reqs: 0,
setup: function (base, fullName, staticProps, protoProps) {
Assume fullName
wasn’t passed. (can.Model.extend({ ... }, { ... })
)
This is pretty usual.
if (typeof fullName !== "string") {
protoProps = staticProps;
staticProps = fullName;
}
Assume no static properties were passed. (can.Model.extend({ ... })
)
This is really unusual for a model though, since there’s so much configuration.
if (!protoProps) {
!steal-remove-start
can.dev.warn("can/model/model.js: can.Model extended without static properties.");
!steal-remove-end
protoProps = staticProps;
}
Create the model store here, in case someone wants to use can.Model without inheriting from it.
this.store = {};
can.Map.setup.apply(this, arguments);
if (!can.Model) {
return;
}
List
is just a regular can.Model.List that knows what kind of Model it’s hooked up to.
if(staticProps && staticProps.List) {
this.List = staticProps.List;
this.List.Map = this;
} else {
this.List = base.List.extend({
Map: this
}, {});
}
var self = this,
clean = can.proxy(this._clean, self);
Go through ajaxMethods
and set up static methods according to their configurations.
can.each(ajaxMethods, function (method, name) {
Check the configuration for this ajaxMethod.
If the configuration isn’t a function, it should be a string (like "GET /endpoint"
)
or an object like {url: "/endpoint", type: 'GET'}
.
if we have a string(like "GET /endpoint"
) or an object(ajaxSettings) set in the static definition(not inherited),
convert it to a function.
if(staticProps && staticProps[name] && (typeof staticProps[name] === 'string' || typeof staticProps[name] === 'object')) {
self[name] = ajaxMaker(method, staticProps[name]);
}
if we have a resource property set in the static definition, but check if function exists already
else if(staticProps && staticProps.resource && !can.isFunction(staticProps[name])) {
self[name] = ajaxMaker(method, createURLFromResource(self, name));
}
There may also be a “maker” function (like makeFindAll
) that alters the behavior of acting upon models
by changing when and how the function we just made with ajaxMaker
gets called.
For example, you might cache responses and only make a call when you don’t have a cached response.
if (self["make" + can.capitalize(name)]) {
Use the “maker” function to make the new “ajaxMethod” function.
var newMethod = self["make" + can.capitalize(name)](self[name]);
Replace the “ajaxMethod” function in the configuration with the new one.
(_overwrite
just overwrites a property in a given Construct.)
can.Construct._overwrite(self, base, name, function () {
Increment the numer of requests…
can.Model._reqs++;
…make the AJAX call (and whatever else you’re doing)…
var def = newMethod.apply(this, arguments);
…and clean up the store.
var then = def.then(clean, clean);
Pass along abort
so you can still abort the AJAX call.
then.abort = def.abort;
return then;
});
}
});
var hasCustomConverter = {};
Set up models
and model
.
can.each(converters, function(converter, name) {
var parseName = "parse" + can.capitalize(name),
dataProperty = (staticProps && staticProps[name]) || self[name];
For legacy e.g. models: ‘someProperty’ we set the parseModel(s)
property
to the given string and set .model(s) to the original converter
if(typeof dataProperty === 'string') {
self[parseName] = dataProperty;
can.Construct._overwrite(self, base, name, converter);
} else if((staticProps && staticProps[name])) {
hasCustomConverter[parseName] = true;
}
});
Sets up parseModel(s)
can.each(makeParser, function(maker, parseName) {
var prop = (staticProps && staticProps[parseName]) || self[parseName];
e.g. parseModels: ‘someProperty’ make a default parseModel(s)
if(typeof prop === 'string') {
can.Construct._overwrite(self, base, parseName, maker(prop));
} else if( (!staticProps || !can.isFunction(staticProps[parseName])) && !self[parseName] ) {
var madeParser = maker();
madeParser.useModelConverter = hasCustomConverter[parseName];
Add a default parseModel(s) if there is none
can.Construct._overwrite(self, base, parseName, madeParser);
}
});
Make sure we have a unique name for this Model.
if (self.fullName === "can.Model" || !self.fullName) {
self.fullName = "Model" + (++modelNum);
}
can.Model._reqs = 0;
this._url = this._shortName + "/{" + this.id + "}";
},
_ajax: ajaxMaker,
_makeRequest: makeRequest,
_clean: function () {
can.Model._reqs--;
Don’t clean up unless we have no pending requests.
if (!can.Model._reqs) {
for (var id in this.store) {
Delete all items in the store without any event bindings.
if (!this.store[id]._bindings) {
delete this.store[id];
}
}
}
return arguments[0];
},
models: converters.models,
model: converters.model
},
/** @prototype */
{
setup: function (attrs) {
Try to add things as early as possible to the store (#457). This is the earliest possible moment, even before any properties are set.
var id = attrs && attrs[this.constructor.id];
if (can.Model._reqs && id != null) {
this.constructor.store[id] = this;
}
can.Map.prototype.setup.apply(this, arguments);
},
isNew: function () {
var id = getId(this);
0 is a valid ID.
TODO: Why not return id === null || id === undefined;
?
return !(id || id === 0); // If `null` or `undefined`
},
save: function (success, error) {
return makeRequest(this, this.isNew() ? 'create' : 'update', success, error);
},
destroy: function (success, error) {
If this model is new, don’t make an AJAX call. Instead, we have to construct the Deferred ourselves and return it.
if (this.isNew()) {
var self = this;
var def = can.Deferred();
def.then(success, error);
return def.done(function (data) {
self.destroyed(data);
}).resolve(self);
}
If it isn’t new, though, go ahead and make a request.
return makeRequest(this, 'destroy', success, error, 'destroyed');
},
These aren’t actually implemented here, but their setup needs to be changed to account for the store.
_bindsetup: function () {
var modelInstance = this.___get(this.constructor.id);
if (modelInstance != null) {
this.constructor.store[modelInstance ] = this;
}
return can.Map.prototype._bindsetup.apply(this, arguments);
},
_bindteardown: function () {
delete this.constructor.store[getId(this)];
return can.Map.prototype._bindteardown.apply(this, arguments);
},
Change the behavior of ___set
to account for the store.
___set: function (prop, val) {
can.Map.prototype.___set.call(this, prop, val);
If we add or change the ID, update the store accordingly. TODO: shouldn’t this also delete the record from the old ID in the store?
if (prop === this.constructor.id && this._bindings) {
this.constructor.store[getId(this)] = this;
}
}
});
Returns a function that knows how to prepare data from findAll
or findOne
calls.
name
should be either model
or models
.
var makeGetterHandler = function (name) {
return function (data, readyState, xhr) {
return this[name](data, null, xhr);
};
},
Handle data returned from create
, update
, and destroy
calls.
createUpdateDestroyHandler = function (data) {
if(this.parseModel.useModelConverter) {
return this.model(data);
}
return this.parseModel(data);
};
var responseHandlers = {
makeFindAll: makeGetterHandler("models"),
makeFindOne: makeGetterHandler("model"),
makeCreate: createUpdateDestroyHandler,
makeUpdate: createUpdateDestroyHandler,
makeDestroy: createUpdateDestroyHandler
};
Go through the response handlers and make the actual “make” methods.
can.each(responseHandlers, function (method, name) {
can.Model[name] = function (oldMethod) {
return function () {
var args = can.makeArray(arguments),
If args[1] is a function, we were only passed one argument before success and failure callbacks.
oldArgs = can.isFunction(args[1]) ? args.splice(0, 1) : args.splice(0, 2),
Call the AJAX method (findAll
or update
, etc.) and pipe it through the response handler from above.
def = pipe(oldMethod.apply(this, oldArgs), this, method);
def.then(args[0], args[1]);
return def;
};
};
});
can.each([
"created",
"updated",
"destroyed"
], function (funcName) {
Each of these is pretty much the same, except for the events they trigger.
can.Model.prototype[funcName] = function (attrs) {
var self = this,
constructor = self.constructor;
Update attributes if attributes have been passed
if(attrs && typeof attrs === 'object') {
this.attr(can.isFunction(attrs.attr) ? attrs.attr() : attrs);
}
triggers change event that bubble’s like handler( ‘change’,’1.destroyed’ ). This is used to remove items on destroyed from Model Lists. but there should be a better way.
can.dispatch.call(this, {type:funcName, target: this}, []);
!steal-remove-start
can.dev.log("Model.js - " + constructor.shortName + " " + funcName);
!steal-remove-end
Call event on the instance’s Class
can.dispatch.call(constructor, funcName, [this]);
};
});
Model Lists are just like Map.List
s except that when their items are
destroyed, they automatically get removed from the List.
var ML = can.Model.List = can.List.extend({
On change or a nested named event, setup change bubbling. On any other type of event, setup “destroyed” bubbling.
_bubbleRule: function(eventName, list) {
var bubbleRules = can.List._bubbleRule(eventName, list);
bubbleRules.push('destroyed');
return bubbleRules;
}
},{
setup: function (params) {
If there was a plain object passed to the List constructor, we use those as parameters for an initial findAll.
if (can.isPlainObject(params) && !can.isArray(params)) {
can.List.prototype.setup.apply(this);
this.replace(can.isPromise(params) ? params : this.constructor.Map.findAll(params));
} else {
Otherwise, set up the list like normal.
can.List.prototype.setup.apply(this, arguments);
}
this.bind('destroyed', can.proxy(this._destroyed, this));
},
_destroyed: function (ev, attr) {
if (/\w+/.test(attr)) {
var index;
while((index = this.indexOf(ev.target)) > -1) {
this.splice(index, 1);
}
}
}
});
return can.Model;
});